@svrnsec/pulse 0.3.0 → 0.4.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.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @sovereign/pulse — Hypervisor & Cloud Provider Fingerprinter
3
+ *
4
+ * Each hypervisor has a distinct "steal-time rhythm" — a characteristic
5
+ * pattern in how it schedules guest vCPUs on host physical cores.
6
+ * This creates detectable signatures in the timing autocorrelation profile.
7
+ *
8
+ * Think of it like a heartbeat EKG:
9
+ * KVM → regular 50-iteration bursts (~250ms quantum at 5ms/iter)
10
+ * Xen → longer 150-iteration bursts (~750ms credit scheduler quantum)
11
+ * VMware → irregular bursts, memory balloon noise
12
+ * Hyper-V → 78-iteration bursts (~390ms at 5ms/iter, 15.6ms quantum)
13
+ * Nitro → almost none — SR-IOV passthrough is nearly invisible
14
+ * Physical → no rhythm at all
15
+ *
16
+ * Canvas renderer strings give a second, independent signal that we cross-
17
+ * reference to increase confidence in the provider classification.
18
+ */
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Provider profile database
22
+ // ---------------------------------------------------------------------------
23
+ // Each profile is calibrated from real benchmark data.
24
+ // Fields: lag1_range, lag50_range, qe_range, cv_range, renderer_hints
25
+
26
+ const PROVIDER_PROFILES = [
27
+ {
28
+ id: 'physical',
29
+ label: 'Physical Hardware',
30
+ profile: 'analog-fog',
31
+ confidence: 0, // set dynamically
32
+ match: ({ lag1, lag50, qe, cv, entropyJitterRatio, isSoftwareRenderer }) =>
33
+ !isSoftwareRenderer &&
34
+ Math.abs(lag1) < 0.20 &&
35
+ Math.abs(lag50) < 0.15 &&
36
+ qe > 3.0 &&
37
+ cv > 0.06 &&
38
+ (entropyJitterRatio === null || entropyJitterRatio >= 1.02),
39
+ },
40
+ {
41
+ id: 'kvm-generic',
42
+ label: 'KVM Hypervisor (generic)',
43
+ profile: 'picket-fence',
44
+ match: ({ lag1, lag50, qe, cv }) =>
45
+ lag1 > 0.40 && qe < 2.5 && cv < 0.15 && Math.abs(lag50) > 0.25,
46
+ providerHints: ['digitalocean', 'linode', 'vultr', 'hetzner', 'ovh'],
47
+ },
48
+ {
49
+ id: 'kvm-digitalocean',
50
+ label: 'DigitalOcean Droplet (KVM)',
51
+ profile: 'picket-fence',
52
+ match: ({ lag1, lag50, qe, cv, rendererHints }) =>
53
+ lag1 > 0.55 && qe < 2.0 && cv < 0.12 &&
54
+ (rendererHints.some(r => ['llvmpipe', 'virtio', 'qxl'].includes(r)) ||
55
+ lag50 > 0.30),
56
+ },
57
+ {
58
+ id: 'kvm-aws-ec2-xen',
59
+ label: 'AWS EC2 (Xen/older generation)',
60
+ profile: 'picket-fence',
61
+ // Xen credit scheduler has longer period (~150 iters)
62
+ match: ({ lag1, lag25, lag50, qe, cv }) =>
63
+ qe < 2.2 && cv < 0.13 &&
64
+ lag25 > 0.20 && lag50 > 0.20 &&
65
+ lag1 < 0.50, // lag-1 less pronounced than KVM
66
+ },
67
+ {
68
+ id: 'nitro-aws',
69
+ label: 'AWS EC2 Nitro (near-baremetal)',
70
+ profile: 'near-physical',
71
+ // Nitro uses SR-IOV and dedicated hardware — steal-time is very low.
72
+ // Looks almost physical but canvas renderer gives it away.
73
+ match: ({ lag1, lag50, qe, cv, isSoftwareRenderer, rendererHints }) =>
74
+ qe > 2.5 && cv > 0.05 &&
75
+ lag1 < 0.25 && lag50 < 0.20 &&
76
+ (isSoftwareRenderer ||
77
+ rendererHints.some(r => r.includes('nvidia t4') || r.includes('nvidia a10'))),
78
+ },
79
+ {
80
+ id: 'vmware-esxi',
81
+ label: 'VMware ESXi',
82
+ profile: 'burst-scheduler',
83
+ // VMware balloon driver creates irregular memory pressure bursts
84
+ match: ({ lag1, lag50, qe, cv, rendererHints }) =>
85
+ qe < 2.5 &&
86
+ (rendererHints.some(r => r.includes('vmware')) ||
87
+ (lag1 > 0.30 && lag50 < lag1 * 0.7 && cv < 0.14)),
88
+ },
89
+ {
90
+ id: 'hyperv',
91
+ label: 'Microsoft Hyper-V',
92
+ profile: 'picket-fence',
93
+ // 15.6ms scheduler quantum → burst every ~78 iters
94
+ match: ({ lag1, lag25, qe, cv, rendererHints }) =>
95
+ qe < 2.3 &&
96
+ (rendererHints.some(r => r.includes('microsoft basic render') || r.includes('warp')) ||
97
+ (lag25 > 0.25 && lag1 > 0.35 && cv < 0.12)),
98
+ },
99
+ {
100
+ id: 'gcp-kvm',
101
+ label: 'Google Cloud (KVM)',
102
+ profile: 'picket-fence',
103
+ match: ({ lag1, lag50, qe, cv, rendererHints }) =>
104
+ qe < 2.3 && lag1 > 0.45 &&
105
+ (rendererHints.some(r => r.includes('swiftshader') || r.includes('google')) ||
106
+ (lag50 > 0.28 && cv < 0.11)),
107
+ },
108
+ {
109
+ id: 'gh200-datacenter',
110
+ label: 'NVIDIA GH200 / HPC Datacenter',
111
+ profile: 'hypervisor-flat',
112
+ // Even with massive compute, still trapped by hypervisor clock.
113
+ // GH200 shows near-zero Hurst (extreme quantization) + very high lag-1.
114
+ match: ({ lag1, qe, hurst, cv, rendererHints }) =>
115
+ (rendererHints.some(r => r.includes('gh200') || r.includes('grace hopper') ||
116
+ r.includes('nvidia a100') || r.includes('nvidia h100')) ||
117
+ (hurst < 0.10 && lag1 > 0.60 && qe < 1.8 && cv < 0.10)),
118
+ },
119
+ {
120
+ id: 'generic-vm',
121
+ label: 'Virtual Machine (unclassified)',
122
+ profile: 'picket-fence',
123
+ match: ({ lag1, qe, cv, isSoftwareRenderer }) =>
124
+ isSoftwareRenderer ||
125
+ (qe < 2.0 && lag1 > 0.35) ||
126
+ (cv < 0.02),
127
+ },
128
+ ];
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // detectProvider
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Classifies the host environment based on timing + canvas signals.
136
+ *
137
+ * @param {object} p
138
+ * @param {import('./jitter.js').JitterAnalysis} p.jitter
139
+ * @param {object} p.autocorrelations - extended lags including lag25, lag50
140
+ * @param {import('../collector/canvas.js').CanvasFingerprint} p.canvas
141
+ * @param {object|null} p.phases
142
+ * @returns {ProviderResult}
143
+ */
144
+ export function detectProvider({ jitter, autocorrelations, canvas, phases }) {
145
+ const rendererHints = _rendererHints(canvas?.webglRenderer, canvas?.webglVendor);
146
+
147
+ const signals = {
148
+ lag1: Math.abs(autocorrelations?.lag1 ?? 0),
149
+ lag25: Math.abs(autocorrelations?.lag25 ?? 0),
150
+ lag50: Math.abs(autocorrelations?.lag50 ?? 0),
151
+ qe: jitter.quantizationEntropy,
152
+ cv: jitter.stats?.cv ?? 0,
153
+ hurst: jitter.hurstExponent ?? 0.5,
154
+ isSoftwareRenderer: canvas?.isSoftwareRenderer ?? false,
155
+ rendererHints,
156
+ entropyJitterRatio: phases?.entropyJitterRatio ?? null,
157
+ };
158
+
159
+ // Score each profile and pick the best match
160
+ const scored = PROVIDER_PROFILES
161
+ .filter(p => {
162
+ try { return p.match(signals); }
163
+ catch { return false; }
164
+ })
165
+ .map(p => ({
166
+ ...p,
167
+ // Physical hardware is the last resort; give it lower priority when
168
+ // other profiles match so we don't misclassify VMs.
169
+ priority: p.id === 'physical' ? 0 : 1,
170
+ }))
171
+ .sort((a, b) => b.priority - a.priority);
172
+
173
+ const best = scored[0] ?? { id: 'unknown', label: 'Unknown', profile: 'unknown' };
174
+
175
+ // Confidence: how many "VM indicator" thresholds the signals cross
176
+ const vmIndicatorCount = [
177
+ signals.qe < 2.5,
178
+ signals.lag1 > 0.35,
179
+ signals.lag50 > 0.20,
180
+ signals.cv < 0.04,
181
+ signals.isSoftwareRenderer,
182
+ signals.hurst < 0.15,
183
+ phases?.entropyJitterRatio != null && phases.entropyJitterRatio < 1.02,
184
+ ].filter(Boolean).length;
185
+
186
+ const isPhysical = best.id === 'physical';
187
+ const confidence = isPhysical
188
+ ? Math.max(20, 95 - vmIndicatorCount * 15)
189
+ : Math.min(95, 40 + vmIndicatorCount * 12);
190
+
191
+ return {
192
+ providerId: best.id,
193
+ providerLabel: best.label,
194
+ profile: best.profile,
195
+ confidence,
196
+ isVirtualized: best.id !== 'physical',
197
+ signals,
198
+ alternatives: scored.slice(1, 3).map(p => ({ id: p.id, label: p.label })),
199
+ rendererHints,
200
+ schedulerQuantumMs: _estimateQuantum(signals),
201
+ };
202
+ }
203
+
204
+ /**
205
+ * @typedef {object} ProviderResult
206
+ * @property {string} providerId
207
+ * @property {string} providerLabel
208
+ * @property {string} profile 'analog-fog' | 'picket-fence' | 'burst-scheduler' | 'near-physical' | 'hypervisor-flat' | 'unknown'
209
+ * @property {number} confidence 0–100
210
+ * @property {boolean} isVirtualized
211
+ * @property {object} signals
212
+ * @property {object[]} alternatives
213
+ * @property {string[]} rendererHints
214
+ * @property {number|null} schedulerQuantumMs
215
+ */
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Internal helpers
219
+ // ---------------------------------------------------------------------------
220
+
221
+ /**
222
+ * Extract lowercase hint tokens from WebGL renderer string for pattern matching.
223
+ */
224
+ function _rendererHints(renderer = '', vendor = '') {
225
+ return `${renderer} ${vendor}`.toLowerCase()
226
+ .split(/[\s\/(),]+/)
227
+ .filter(t => t.length > 2);
228
+ }
229
+
230
+ /**
231
+ * Estimate the hypervisor's scheduler quantum from the dominant autocorrelation lag.
232
+ * Returns null if the device appears to be physical.
233
+ */
234
+ function _estimateQuantum({ lag1, lag25, lag50, qe }) {
235
+ if (qe > 3.2) return null; // likely physical
236
+
237
+ // Find the dominant lag (highest absolute autocorrelation beyond lag-5)
238
+ const lags = [
239
+ { lag: 50, ac: lag50 },
240
+ { lag: 25, ac: lag25 },
241
+ ];
242
+ const peak = lags.reduce((b, c) => c.ac > b.ac ? c : b, { lag: 0, ac: 0 });
243
+
244
+ if (peak.ac < 0.20) return null;
245
+
246
+ // Quantum (ms) ≈ dominant_lag × estimated_iteration_time (≈5ms)
247
+ return peak.lag * 5;
248
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * @svrnsec/pulse — TrustScore Engine
3
+ *
4
+ * Converts the raw multi-signal proof into a single 0–100 integer that
5
+ * security teams can put in dashboards, set thresholds on, and alert from.
6
+ *
7
+ * Design goals
8
+ * ────────────
9
+ * 1. Transparent — every point can be traced to a physical measurement.
10
+ * 2. Conservative — a missing signal lowers the score; it never inflates it.
11
+ * 3. Hard floors — certain signals (EJR forgery, software GPU, no grid) can
12
+ * never be compensated by a perfect bio score. Physics overrules behaviour.
13
+ * 4. Grade labels map to enterprise risk tiers (A/B/C/D/F).
14
+ *
15
+ * Signal weights
16
+ * ──────────────
17
+ * Physics layer 40 pts (EJR, Hurst coherence, CV-entropy, autocorr)
18
+ * ENF 20 pts (grid signal presence and region confidence)
19
+ * GPU 15 pts (thermal variance, software renderer penalty)
20
+ * DRAM 15 pts (DDR4 refresh cycle detection)
21
+ * Bio / LLM 10 pts (behavioral biometrics, AI agent detection)
22
+ *
23
+ * Hard floors
24
+ * ───────────
25
+ * EJR forgery detected → score capped at 20 (HARD_KILL)
26
+ * Software GPU renderer → score capped at 45 (likely VM/container)
27
+ * LLM agent conf > 0.85 → score capped at 30 (AI-driven session)
28
+ * No bio activity + no ENF → score capped at 55 (cannot confirm human)
29
+ */
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Grade thresholds
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const GRADES = [
36
+ { min: 90, grade: 'A', label: 'Trusted', color: 'bgreen' },
37
+ { min: 75, grade: 'B', label: 'Verified', color: 'bgreen' },
38
+ { min: 60, grade: 'C', label: 'Marginal', color: 'byellow' },
39
+ { min: 45, grade: 'D', label: 'Suspicious', color: 'byellow' },
40
+ { min: 0, grade: 'F', label: 'Blocked', color: 'bred' },
41
+ ];
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // computeTrustScore
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Compute a 0–100 TrustScore from a ProofPayload + optional extended signals.
49
+ *
50
+ * @param {object} payload - ProofPayload from buildProof()
51
+ * @param {object} [extended] - { enf, gpu, dram, llm } from pulse() extended
52
+ * @returns {TrustScore}
53
+ */
54
+ export function computeTrustScore(payload, extended = {}) {
55
+ const signals = payload?.signals ?? {};
56
+ const cls = payload?.classification ?? {};
57
+ const { enf, gpu, dram, llm } = extended;
58
+
59
+ const breakdown = {};
60
+ const penalties = [];
61
+ const bonuses = [];
62
+ let hardCap = 100;
63
+
64
+ // ── 1. Physics layer (40 pts) ─────────────────────────────────────────────
65
+ const phys = _scorePhysics(signals.entropy, cls);
66
+ breakdown.physics = phys;
67
+
68
+ // Hard kill: EJR forgery
69
+ if (cls.vmIndicators?.includes('ejr_forgery') ||
70
+ phys.ejrForgery) {
71
+ hardCap = Math.min(hardCap, 20);
72
+ penalties.push({ signal: 'physics', reason: 'EJR phase trajectory forgery detected', cap: 20 });
73
+ }
74
+
75
+ // ── 2. ENF layer (20 pts) ─────────────────────────────────────────────────
76
+ const enfScore = _scoreEnf(signals.enf ?? enf);
77
+ breakdown.enf = enfScore;
78
+
79
+ if (enfScore.isVmIndicator) {
80
+ penalties.push({ signal: 'enf', reason: 'No grid signal — conditioned/datacenter power', cap: 70 });
81
+ hardCap = Math.min(hardCap, 70);
82
+ }
83
+
84
+ // ── 3. GPU layer (15 pts) ─────────────────────────────────────────────────
85
+ const gpuScore = _scoreGpu(signals.gpu ?? gpu);
86
+ breakdown.gpu = gpuScore;
87
+
88
+ if (gpuScore.isSoftware) {
89
+ hardCap = Math.min(hardCap, 45);
90
+ penalties.push({ signal: 'gpu', reason: 'Software renderer detected (SwiftShader/llvmpipe)', cap: 45 });
91
+ }
92
+
93
+ // ── 4. DRAM layer (15 pts) ────────────────────────────────────────────────
94
+ const dramScore = _scoreDram(signals.dram ?? dram);
95
+ breakdown.dram = dramScore;
96
+
97
+ // ── 5. Bio / LLM layer (10 pts) ───────────────────────────────────────────
98
+ const bioScore = _scoreBio(signals.bio, signals.llm ?? llm);
99
+ breakdown.bio = bioScore;
100
+
101
+ if (bioScore.aiConf > 0.85) {
102
+ hardCap = Math.min(hardCap, 30);
103
+ penalties.push({ signal: 'bio', reason: `AI agent detected (conf ${(bioScore.aiConf * 100).toFixed(0)}%)`, cap: 30 });
104
+ }
105
+
106
+ // No bio + no ENF = can't confirm human on real device
107
+ if (!signals.bio?.hasActivity && !signals.enf?.ripplePresent) {
108
+ hardCap = Math.min(hardCap, 55);
109
+ penalties.push({ signal: 'bio+enf', reason: 'No bio activity and no grid signal', cap: 55 });
110
+ }
111
+
112
+ // ── Bonuses ───────────────────────────────────────────────────────────────
113
+ // Temporal anchor present (ENF deviation logged → session timestampable)
114
+ if (signals.enf?.capturedAt && signals.enf?.enfDeviation != null) {
115
+ bonuses.push({ signal: 'enf', reason: 'Temporal fingerprint present', pts: 2 });
116
+ }
117
+ // Both GPU and DRAM confirmed physical
118
+ if (gpuScore.pts >= 13 && dramScore.pts >= 12) {
119
+ bonuses.push({ signal: 'gpu+dram', reason: 'GPU thermal + DRAM refresh both confirmed', pts: 3 });
120
+ }
121
+
122
+ // ── Raw score ─────────────────────────────────────────────────────────────
123
+ const bonusPts = bonuses.reduce((s, b) => s + b.pts, 0);
124
+ const raw = Math.min(100,
125
+ phys.pts +
126
+ enfScore.pts +
127
+ gpuScore.pts +
128
+ dramScore.pts +
129
+ bioScore.pts +
130
+ bonusPts
131
+ );
132
+
133
+ // Apply hard cap
134
+ const score = Math.max(0, Math.min(hardCap, raw));
135
+
136
+ // ── Grade ─────────────────────────────────────────────────────────────────
137
+ const gradeEntry = GRADES.find(g => score >= g.min) ?? GRADES[GRADES.length - 1];
138
+
139
+ return {
140
+ score,
141
+ grade: gradeEntry.grade,
142
+ label: gradeEntry.label,
143
+ color: gradeEntry.color,
144
+ hardCap: hardCap < 100 ? hardCap : null,
145
+ breakdown,
146
+ penalties,
147
+ bonuses,
148
+ // Convenience: per-layer pct (0–1)
149
+ signals: {
150
+ physics: +(phys.pts / 40).toFixed(3),
151
+ enf: +(enfScore.pts / 20).toFixed(3),
152
+ gpu: +(gpuScore.pts / 15).toFixed(3),
153
+ dram: +(dramScore.pts / 15).toFixed(3),
154
+ bio: +(bioScore.pts / 10).toFixed(3),
155
+ },
156
+ };
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Per-signal scorers
161
+ // ---------------------------------------------------------------------------
162
+
163
+ function _scorePhysics(entropy, cls) {
164
+ let pts = 0;
165
+ let ejrForgery = false;
166
+
167
+ if (!entropy) return { pts: 0, max: 40, ejrForgery, reason: 'no entropy data' };
168
+
169
+ const jitter = cls.jitterScore ?? 0;
170
+ const qe = entropy.quantizationEntropy ?? 0;
171
+ const hurst = entropy.hurstExponent ?? 0.5;
172
+ const cv = entropy.timingsCV ?? 0;
173
+ const lag1 = entropy.autocorr_lag1 ?? 0;
174
+
175
+ // Jitter score (0–15 pts)
176
+ pts += Math.round(Math.min(1, Math.max(0, jitter)) * 15);
177
+
178
+ // QE / EJR (0–10 pts)
179
+ if (qe >= 4.0) pts += 10;
180
+ else if (qe >= 3.0) pts += 8;
181
+ else if (qe >= 2.0) pts += 5;
182
+ else if (qe >= 1.5) pts += 2;
183
+ else if (qe < 1.08) { ejrForgery = true; pts += 0; }
184
+
185
+ // Hurst coherence (0–8 pts)
186
+ const hurstDelta = Math.abs(hurst - 0.5);
187
+ if (hurstDelta < 0.05) pts += 8;
188
+ else if (hurstDelta < 0.10) pts += 5;
189
+ else if (hurstDelta < 0.20) pts += 2;
190
+
191
+ // CV-entropy coherence (0–7 pts) — high CV must accompany high QE
192
+ const cvOk = cv > 0.08 && qe > 2.5;
193
+ if (cvOk) pts += 7;
194
+ else if (cv > 0.05 && qe > 1.8) pts += 3;
195
+
196
+ // Autocorrelation (bonus/penalty on existing score)
197
+ if (Math.abs(lag1) < 0.10) pts = Math.min(40, pts + 2);
198
+ if (lag1 > 0.60) pts = Math.max(0, pts - 5);
199
+
200
+ return { pts: Math.min(40, Math.max(0, pts)), max: 40, ejrForgery };
201
+ }
202
+
203
+ function _scoreEnf(enf) {
204
+ if (!enf || enf.available === false || enf.enfAvailable === false) {
205
+ return { pts: 8, max: 20, reason: 'ENF unavailable (no COOP+COEP)', isVmIndicator: false };
206
+ // Unavailable ≠ VM. Give half marks — cannot confirm but cannot deny.
207
+ }
208
+
209
+ let pts = 0;
210
+ const isVmIndicator = enf.isVmIndicator ?? false;
211
+
212
+ if (!enf.ripplePresent) {
213
+ // High sample rate + no ripple = conditioned DC power (datacenter)
214
+ return { pts: 0, max: 20, reason: 'No grid ripple detected', isVmIndicator };
215
+ }
216
+
217
+ // Grid signal present
218
+ pts += 10;
219
+
220
+ // Region confirmed
221
+ if (enf.gridRegion === 'americas' || enf.gridRegion === 'emea_apac') pts += 5;
222
+
223
+ // SNR quality
224
+ const snr = Math.max(enf.snr50hz ?? 0, enf.snr60hz ?? 0);
225
+ if (snr >= 5) pts += 5;
226
+ else if (snr >= 3) pts += 3;
227
+ else if (snr >= 2) pts += 1;
228
+
229
+ return { pts: Math.min(20, pts), max: 20, gridRegion: enf.gridRegion, snr, isVmIndicator };
230
+ }
231
+
232
+ function _scoreGpu(gpu) {
233
+ if (!gpu || gpu.available === false || gpu.gpuPresent === false) {
234
+ return { pts: 8, max: 15, reason: 'WebGPU unavailable', isSoftware: false };
235
+ }
236
+
237
+ if (gpu.isSoftware || gpu.isSoftware === true) {
238
+ return { pts: 0, max: 15, reason: 'Software renderer', isSoftware: true };
239
+ }
240
+
241
+ let pts = 8; // GPU present, not software
242
+
243
+ // Thermal growth (real GPUs warm under load)
244
+ const growth = gpu.thermalGrowth ?? 0;
245
+ if (growth >= 0.05) pts += 7;
246
+ else if (growth >= 0.02) pts += 4;
247
+ else if (growth >= 0.01) pts += 1;
248
+
249
+ return { pts: Math.min(15, pts), max: 15, thermalGrowth: growth, isSoftware: false };
250
+ }
251
+
252
+ function _scoreDram(dram) {
253
+ if (!dram) return { pts: 8, max: 15, reason: 'DRAM probe unavailable' };
254
+
255
+ if (!dram.refreshPresent) {
256
+ return { pts: 0, max: 15, reason: 'No DRAM refresh cycle detected (virtual memory)' };
257
+ }
258
+
259
+ let pts = 10;
260
+
261
+ // Refresh period accuracy (DDR4 nominal = 7.8 ms ± 1.5 ms)
262
+ const period = dram.refreshPeriodMs ?? 0;
263
+ if (period > 0) {
264
+ const delta = Math.abs(period - 7.8);
265
+ if (delta < 0.5) pts += 5;
266
+ else if (delta < 1.0) pts += 3;
267
+ else if (delta < 1.5) pts += 1;
268
+ }
269
+
270
+ // Peak power confidence
271
+ if ((dram.peakPower ?? 0) > 0.3) pts = Math.min(15, pts + 2);
272
+
273
+ return { pts: Math.min(15, pts), max: 15, refreshPeriodMs: period };
274
+ }
275
+
276
+ function _scoreBio(bio, llm) {
277
+ let pts = 5; // neutral baseline when no bio activity
278
+
279
+ const aiConf = llm?.aiConf ?? 0;
280
+
281
+ if (!bio?.hasActivity) {
282
+ // No interaction — can't confirm human but also can't confirm bot
283
+ return { pts, max: 10, aiConf, reason: 'no bio activity' };
284
+ }
285
+
286
+ pts = 7; // bio activity present
287
+
288
+ // High correction rate is a human signal
289
+ const corrRate = llm?.correctionRate ?? 0.08;
290
+ if (corrRate >= 0.05 && corrRate <= 0.20) pts += 2;
291
+
292
+ // Rhythmicity (tremor present)
293
+ const rhythmicity = llm?.rhythmicity ?? 0;
294
+ if (rhythmicity > 0.3) pts += 1;
295
+
296
+ // LLM penalty
297
+ if (aiConf > 0.85) pts = 0;
298
+ else if (aiConf > 0.70) pts = Math.max(0, pts - 4);
299
+ else if (aiConf > 0.50) pts = Math.max(0, pts - 2);
300
+
301
+ return { pts: Math.min(10, Math.max(0, pts)), max: 10, aiConf };
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // formatTrustScore — pretty one-liner for logs
306
+ // ---------------------------------------------------------------------------
307
+
308
+ /**
309
+ * Returns a short human-readable summary.
310
+ * e.g. "TrustScore 87/100 B · Verified [physics:91% enf:80% gpu:100%]"
311
+ */
312
+ export function formatTrustScore(ts) {
313
+ if (!ts) return 'TrustScore N/A';
314
+ const sigs = Object.entries(ts.signals ?? {})
315
+ .map(([k, v]) => `${k}:${Math.round(v * 100)}%`)
316
+ .join(' ');
317
+ return `TrustScore ${ts.score}/100 ${ts.grade} · ${ts.label} [${sigs}]`;
318
+ }
319
+
320
+ /**
321
+ * @typedef {object} TrustScore
322
+ * @property {number} score 0–100
323
+ * @property {string} grade A|B|C|D|F
324
+ * @property {string} label Trusted|Verified|Marginal|Suspicious|Blocked
325
+ * @property {string} color ANSI color name for terminal rendering
326
+ * @property {number|null} hardCap applied hard cap (null if none)
327
+ * @property {object} breakdown per-layer detailed scores
328
+ * @property {object[]} penalties hard cap reasons
329
+ * @property {object[]} bonuses bonus point sources
330
+ * @property {object} signals per-layer 0–1 confidence values
331
+ */
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Minimal argument parser — zero dependencies.
3
+ * Supports: flags (--flag), options (--key value), positional args.
4
+ */
5
+ export function parseArgs(argv = process.argv.slice(2)) {
6
+ const flags = new Set();
7
+ const opts = {};
8
+ const pos = [];
9
+
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const arg = argv[i];
12
+ if (arg.startsWith('--')) {
13
+ const key = arg.slice(2);
14
+ const next = argv[i + 1];
15
+ if (next && !next.startsWith('--')) {
16
+ opts[key] = next;
17
+ i++;
18
+ } else {
19
+ flags.add(key);
20
+ }
21
+ } else if (arg.startsWith('-') && arg.length === 2) {
22
+ flags.add(arg.slice(1));
23
+ } else {
24
+ pos.push(arg);
25
+ }
26
+ }
27
+
28
+ return {
29
+ command: pos[0] ?? null,
30
+ positional: pos.slice(1),
31
+ flags,
32
+ opts,
33
+ has: (f) => flags.has(f),
34
+ get: (k, def) => opts[k] ?? def,
35
+ };
36
+ }