react-pref-guard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1609 @@
1
+ // src/PerfProvider.tsx
2
+ import { useEffect as useEffect2, useState as useState2 } from "react";
3
+
4
+ // src/collector.ts
5
+ var buffer = /* @__PURE__ */ new Map();
6
+ function collectMetric(metric2) {
7
+ const {
8
+ component,
9
+ phase,
10
+ actualDuration,
11
+ boundaryType
12
+ } = metric2;
13
+ let entry = buffer.get(component);
14
+ if (!entry) {
15
+ entry = {
16
+ renders: 0,
17
+ totalTime: 0,
18
+ maxTime: 0,
19
+ boundaryType,
20
+ phaseCounts: { mount: 0, update: 0 }
21
+ };
22
+ buffer.set(component, entry);
23
+ }
24
+ entry.renders += 1;
25
+ entry.totalTime += actualDuration;
26
+ entry.maxTime = Math.max(entry.maxTime, actualDuration);
27
+ if (phase === "mount" || phase === "update") {
28
+ entry.phaseCounts[phase] += 1;
29
+ }
30
+ }
31
+ function flushMetrics() {
32
+ const now = performance.now();
33
+ const snapshot = [];
34
+ for (const [component, metric2] of buffer.entries()) {
35
+ snapshot.push({
36
+ component,
37
+ renders: metric2.renders,
38
+ avgTime: metric2.renders > 0 ? metric2.totalTime / metric2.renders : 0,
39
+ maxTime: metric2.maxTime,
40
+ boundaryType: metric2.boundaryType,
41
+ phaseCounts: {
42
+ mount: metric2.phaseCounts.mount,
43
+ update: metric2.phaseCounts.update
44
+ },
45
+ timestamp: now
46
+ });
47
+ }
48
+ buffer.clear();
49
+ return snapshot;
50
+ }
51
+
52
+ // src/issue-store/issueStore.ts
53
+ var store = /* @__PURE__ */ new Map();
54
+ var listeners = /* @__PURE__ */ new Set();
55
+ function upsertIssue(issue) {
56
+ store.set(issue.id, issue);
57
+ notify();
58
+ }
59
+ function resolveIssue(id) {
60
+ const row = store.get(id);
61
+ if (!row) return;
62
+ row.status = "RESOLVED";
63
+ notify();
64
+ setTimeout(() => {
65
+ store.delete(id);
66
+ notify();
67
+ }, 1e4);
68
+ }
69
+ function subscribe(listener) {
70
+ listeners.add(listener);
71
+ listener(Array.from(store.values()));
72
+ return () => listeners.delete(listener);
73
+ }
74
+ function notify() {
75
+ const snapshot = Array.from(store.values());
76
+ listeners.forEach((l) => l(snapshot));
77
+ }
78
+
79
+ // src/warnings.ts
80
+ var activeIssues = /* @__PURE__ */ new Map();
81
+ var lastLogTime = /* @__PURE__ */ new Map();
82
+ var LOG_COOLDOWN = 3e4;
83
+ var RESOLVE_AFTER_MISSES = 3;
84
+ function fingerprint(component, issue) {
85
+ return `${component}:${issue.ruleId}:${issue.severity}`;
86
+ }
87
+ function shouldLog(fp) {
88
+ const last = lastLogTime.get(fp) || 0;
89
+ if (Date.now() - last < LOG_COOLDOWN) return false;
90
+ lastLogTime.set(fp, Date.now());
91
+ return true;
92
+ }
93
+ function severityEmoji(sev) {
94
+ switch (sev) {
95
+ case "CRITICAL":
96
+ return "\u{1F4A5}";
97
+ case "HIGH":
98
+ return "\u{1F534}";
99
+ case "MEDIUM":
100
+ return "\u{1F7E1}";
101
+ case "LOW":
102
+ return "\u{1F535}";
103
+ default:
104
+ return "\u2139\uFE0F";
105
+ }
106
+ }
107
+ function severityColor(sev) {
108
+ switch (sev) {
109
+ case "CRITICAL":
110
+ return "#dc2626";
111
+ case "HIGH":
112
+ return "#ef4444";
113
+ case "MEDIUM":
114
+ return "#f59e0b";
115
+ case "LOW":
116
+ return "#3b82f6";
117
+ default:
118
+ return "#6b7280";
119
+ }
120
+ }
121
+ function showWarning(result) {
122
+ const seenThisBatch = /* @__PURE__ */ new Set();
123
+ for (const issue of result.issues) {
124
+ const fp = fingerprint(result.component, issue);
125
+ seenThisBatch.add(fp);
126
+ const state = activeIssues.get(fp);
127
+ if (!state) {
128
+ activeIssues.set(fp, { issue, missingCount: 0 });
129
+ upsertIssue({
130
+ id: fp,
131
+ component: result.component,
132
+ ruleId: issue.ruleId,
133
+ severity: issue.severity,
134
+ confidence: issue.confidence,
135
+ boundaryType: result.boundaryType,
136
+ status: "NEW",
137
+ reason: issue.reason,
138
+ lastSeen: Date.now()
139
+ });
140
+ if (shouldLog(fp)) {
141
+ logNewIssue(result, issue);
142
+ }
143
+ } else {
144
+ state.missingCount = 0;
145
+ upsertIssue({
146
+ id: fp,
147
+ component: result.component,
148
+ ruleId: issue.ruleId,
149
+ severity: issue.severity,
150
+ // CRITICAL preserved
151
+ confidence: issue.confidence,
152
+ boundaryType: result.boundaryType,
153
+ status: "ACTIVE",
154
+ reason: issue.reason,
155
+ lastSeen: Date.now()
156
+ });
157
+ }
158
+ }
159
+ for (const [fp, state] of activeIssues.entries()) {
160
+ if (!seenThisBatch.has(fp)) {
161
+ state.missingCount++;
162
+ if (state.missingCount >= RESOLVE_AFTER_MISSES) {
163
+ activeIssues.delete(fp);
164
+ resolveIssue(fp);
165
+ logResolvedIssue(result.component, state.issue);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ function showCriticalAlert(result) {
171
+ for (const issue of result.issues) {
172
+ if (issue.severity !== "CRITICAL") continue;
173
+ const fp = fingerprint(result.component, issue);
174
+ if (!shouldLog(fp)) continue;
175
+ showCriticalOverlay(result.component, issue);
176
+ }
177
+ }
178
+ function logNewIssue(result, issue) {
179
+ console.groupCollapsed(
180
+ `%c\u26A1 PerfGuard \xB7 ${result.component}`,
181
+ `color:${severityColor(issue.severity)};font-weight:bold`
182
+ );
183
+ console.info(
184
+ `%c${severityEmoji(issue.severity)} ${issue.ruleId} (${issue.severity})`,
185
+ `color:${severityColor(issue.severity)}`
186
+ );
187
+ console.info("Confidence:", `${Math.round(issue.confidence * 100)}%`);
188
+ console.info("Reason:", issue.reason);
189
+ console.info("Boundary:", result.boundaryType);
190
+ console.groupCollapsed("\u{1F4CA} Metrics");
191
+ console.table(result.metrics);
192
+ console.groupEnd();
193
+ console.groupEnd();
194
+ }
195
+ function logResolvedIssue(component, issue) {
196
+ console.info(
197
+ `%c\u2705 PerfGuard \xB7 RESOLVED \xB7 ${component} \xB7 ${issue.ruleId}`,
198
+ "color:#16a34a;font-weight:bold"
199
+ );
200
+ }
201
+ function showCriticalOverlay(component, issue) {
202
+ if (typeof document === "undefined") return;
203
+ const alert = document.createElement("div");
204
+ alert.style.cssText = `
205
+ position: fixed;
206
+ bottom: 20px;
207
+ right: 20px;
208
+ background: #dc2626;
209
+ color: white;
210
+ padding: 14px 18px;
211
+ border-radius: 8px;
212
+ box-shadow: 0 10px 20px rgba(0,0,0,0.35);
213
+ z-index: 10000;
214
+ font-family: monospace;
215
+ font-size: 13px;
216
+ max-width: 360px;
217
+ `;
218
+ alert.innerHTML = `
219
+ <strong>\u{1F4A5} PerfGuard \u2013 CRITICAL</strong><br/>
220
+ Component: ${component}<br/>
221
+ Rule: ${issue.ruleId}<br/>
222
+ Confidence: ${Math.round(issue.confidence * 100)}%
223
+ `;
224
+ document.body.appendChild(alert);
225
+ setTimeout(() => alert.remove(), 8e3);
226
+ }
227
+
228
+ // src/worker/createWorker.ts
229
+ function createAnalyzerWorker() {
230
+ const code = `
231
+ /* ===============================
232
+ PerfGuard \u2013 Enhanced Rule DSL Worker
233
+ =============================== */
234
+
235
+ const history = new Map();
236
+ const MAX_HISTORY = 10; // Increased for trend detection
237
+
238
+ const DOMINANT_RULES = new Set([
239
+ "BLOCKING_RENDER",
240
+ "SEVERE_PERF_REGRESSION",
241
+ "SUSPICIOUS_RENDER_LOOP",
242
+ "RENDER_TIME_CREEP",
243
+ ]);
244
+
245
+ let RULES = [];
246
+
247
+ /* -------------------------------
248
+ Utilities
249
+ -------------------------------- */
250
+
251
+ function hasDominantIssue(issues) {
252
+ return issues.some(issue => DOMINANT_RULES.has(issue.ruleId));
253
+ }
254
+
255
+ function record(snapshot) {
256
+ const list = history.get(snapshot.component) || [];
257
+ list.push(snapshot);
258
+ if (list.length > MAX_HISTORY) list.shift();
259
+ history.set(snapshot.component, list);
260
+ }
261
+
262
+ function calculateConfidenceWithCurrent(predicateFn, historyList, snapshot) {
263
+ if (historyList.length === 0) return 1;
264
+
265
+ const matches = historyList.filter(predicateFn).length;
266
+ const currentMatch = predicateFn(snapshot) ? 1 : 0;
267
+
268
+ return (matches + currentMatch) / (historyList.length + 1);
269
+ }
270
+
271
+ function downgradeSeverity(severity, confidence, boundaryType) {
272
+ // CRITICAL is NEVER downgraded
273
+ if (severity === "CRITICAL") return "CRITICAL";
274
+
275
+ // INLINE components downgrade by ONE level only
276
+ if (boundaryType === "INLINE") {
277
+ if (severity === "HIGH") return "MEDIUM";
278
+ if (severity === "MEDIUM") return "LOW";
279
+ return "INFO";
280
+ }
281
+
282
+ // \u{1F4C9} Confidence-based softening
283
+ if (confidence < 0.7) return "LOW";
284
+ if (confidence < 0.85) return "MEDIUM";
285
+
286
+ return severity;
287
+ }
288
+
289
+ function buildPredicate({ field, operator, value }) {
290
+ switch (operator) {
291
+ case ">": return (s) => s[field] > value;
292
+ case "<": return (s) => s[field] < value;
293
+ case ">=": return (s) => s[field] >= value;
294
+ case "<=": return (s) => s[field] <= value;
295
+ case "===": return (s) => s[field] === value;
296
+ default: return () => false;
297
+ }
298
+ }
299
+
300
+ function interpolateMessage(template, values) {
301
+ let result = template;
302
+ for (const [key, value] of Object.entries(values)) {
303
+ result = result.replace(
304
+ new RegExp(\`{\${key}}\`, 'g'),
305
+ typeof value === 'number' ? Math.round(value).toString() : value
306
+ );
307
+ }
308
+ return result;
309
+ }
310
+
311
+ /* -------------------------------
312
+ Trend Detection
313
+ -------------------------------- */
314
+
315
+ function detectTrend(historyList, field) {
316
+ if (historyList.length < 5) return { direction: 'stable', change: 0 };
317
+
318
+ // Use linear regression to detect trend
319
+ const values = historyList.map(s => s[field]);
320
+ const n = values.length;
321
+ const indices = Array.from({ length: n }, (_, i) => i);
322
+
323
+ const sumX = indices.reduce((a, b) => a + b, 0);
324
+ const sumY = values.reduce((a, b) => a + b, 0);
325
+ const sumXY = indices.reduce((sum, x, i) => sum + x * values[i], 0);
326
+ const sumX2 = indices.reduce((sum, x) => sum + x * x, 0);
327
+
328
+ const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
329
+ const avgValue = sumY / n;
330
+
331
+ // Calculate percentage change
332
+ const percentChange = (slope * n / avgValue) * 100;
333
+
334
+ return {
335
+ direction: slope > 0 ? 'increasing' : slope < 0 ? 'decreasing' : 'stable',
336
+ change: Math.abs(percentChange)
337
+ };
338
+ }
339
+
340
+ /* -------------------------------
341
+ Rule Evaluation Engine
342
+ -------------------------------- */
343
+
344
+ function evaluate(snapshot) {
345
+ const issues = [];
346
+ const historyList = history.get(snapshot.component) || [];
347
+ const previous = historyList[historyList.length - 1];
348
+
349
+ /* ===============================
350
+ PASS 1: DOMINANT RULES ONLY
351
+ =============================== */
352
+ for (const rule of RULES) {
353
+ if (!DOMINANT_RULES.has(rule.id)) continue;
354
+
355
+ /* ----- Regression Rule ----- */
356
+ if (rule.regression && previous) {
357
+ const field = rule.regression.field;
358
+ const multiplier = rule.regression.multiplier;
359
+
360
+ if (snapshot[field] > previous[field] * multiplier) {
361
+ issues.push({
362
+ ruleId: rule.id,
363
+ confidence: 1,
364
+ severity: downgradeSeverity(
365
+ rule.baseSeverity,
366
+ 1,
367
+ snapshot.boundaryType || "HOC"
368
+ ),
369
+ reason: interpolateMessage(rule.messageTemplate, {
370
+ prevValue: previous[field].toFixed(1),
371
+ currValue: snapshot[field].toFixed(1),
372
+ }),
373
+ });
374
+ }
375
+ continue;
376
+ }
377
+
378
+ /* ----- Trend Detection Rule ----- */
379
+ if (rule.trend && historyList.length >= 5) {
380
+ const trend = detectTrend(historyList, rule.trend.field);
381
+
382
+ if (
383
+ trend.direction === rule.trend.direction &&
384
+ trend.change >= rule.trend.threshold
385
+ ) {
386
+ const confidence = Math.min(trend.change / 100, 1.0);
387
+
388
+ issues.push({
389
+ ruleId: rule.id,
390
+ confidence,
391
+ severity: downgradeSeverity(
392
+ rule.baseSeverity,
393
+ confidence,
394
+ snapshot.boundaryType || "HOC"
395
+ ),
396
+ reason: interpolateMessage(rule.messageTemplate, {
397
+ change: trend.change.toFixed(1),
398
+ }),
399
+ });
400
+ }
401
+ continue;
402
+ }
403
+
404
+ /* ----- Predicate Rule ----- */
405
+ if (rule.predicate) {
406
+ const predicateFn = buildPredicate(rule.predicate);
407
+ const confidence = calculateConfidenceWithCurrent(
408
+ predicateFn,
409
+ historyList,
410
+ snapshot
411
+ );
412
+ const threshold = rule.confidenceThreshold || 0.6;
413
+
414
+ if (predicateFn(snapshot) && confidence >= threshold) {
415
+ issues.push({
416
+ ruleId: rule.id,
417
+ confidence,
418
+ severity: downgradeSeverity(
419
+ rule.baseSeverity,
420
+ confidence,
421
+ snapshot.boundaryType || "HOC"
422
+ ),
423
+ reason: interpolateMessage(rule.messageTemplate, {
424
+ confidence: (confidence * 100).toFixed(0),
425
+ }),
426
+ });
427
+ }
428
+ }
429
+ }
430
+
431
+ /* ===============================
432
+ If dominant issue exists \u2192 stop
433
+ =============================== */
434
+ if (issues.length) {
435
+ // Allow DEV_HINT rules only
436
+ for (const rule of RULES) {
437
+ if (!rule.id.startsWith("DEV_HINT")) continue;
438
+ if (!rule.predicate) continue;
439
+
440
+ const predicateFn = buildPredicate(rule.predicate);
441
+ const confidence = calculateConfidenceWithCurrent(
442
+ predicateFn,
443
+ historyList,
444
+ snapshot
445
+ );
446
+ const threshold = rule.confidenceThreshold || 0.6;
447
+
448
+ if (predicateFn(snapshot) && confidence >= threshold) {
449
+ issues.push({
450
+ ruleId: rule.id,
451
+ confidence,
452
+ severity: "INFO",
453
+ reason: interpolateMessage(rule.messageTemplate, {
454
+ confidence: (confidence * 100).toFixed(0),
455
+ }),
456
+ });
457
+ }
458
+ }
459
+
460
+ record(snapshot);
461
+ return issues;
462
+ }
463
+
464
+ /* ===============================
465
+ PASS 2: NORMAL RULES
466
+ =============================== */
467
+ for (const rule of RULES) {
468
+
469
+ /* ----- Regression Rule ----- */
470
+ if (rule.regression && previous) {
471
+ const field = rule.regression.field;
472
+ const multiplier = rule.regression.multiplier;
473
+
474
+ if (snapshot[field] > previous[field] * multiplier) {
475
+ issues.push({
476
+ ruleId: rule.id,
477
+ confidence: 1,
478
+ severity: downgradeSeverity(
479
+ rule.baseSeverity,
480
+ 1,
481
+ snapshot.boundaryType || "HOC"
482
+ ),
483
+ reason: interpolateMessage(rule.messageTemplate, {
484
+ prevValue: previous[field].toFixed(1),
485
+ currValue: snapshot[field].toFixed(1),
486
+ }),
487
+ });
488
+ }
489
+ continue;
490
+ }
491
+
492
+ /* ----- Trend Detection Rule ----- */
493
+ if (rule.trend && historyList.length >= 5) {
494
+ const trend = detectTrend(historyList, rule.trend.field);
495
+
496
+ if (
497
+ trend.direction === rule.trend.direction &&
498
+ trend.change >= rule.trend.threshold
499
+ ) {
500
+ const confidence = Math.min(trend.change / 100, 1.0);
501
+
502
+ issues.push({
503
+ ruleId: rule.id,
504
+ confidence,
505
+ severity: downgradeSeverity(
506
+ rule.baseSeverity,
507
+ confidence,
508
+ snapshot.boundaryType || "HOC"
509
+ ),
510
+ reason: interpolateMessage(rule.messageTemplate, {
511
+ change: trend.change.toFixed(1),
512
+ }),
513
+ });
514
+ }
515
+ continue;
516
+ }
517
+
518
+ /* ----- Predicate Rule ----- */
519
+ if (rule.predicate) {
520
+ const predicateFn = buildPredicate(rule.predicate);
521
+ const confidence = calculateConfidenceWithCurrent(
522
+ predicateFn,
523
+ historyList,
524
+ snapshot
525
+ );
526
+ const threshold = rule.confidenceThreshold || 0.6;
527
+
528
+ if (predicateFn(snapshot) && confidence >= threshold) {
529
+ issues.push({
530
+ ruleId: rule.id,
531
+ confidence,
532
+ severity: downgradeSeverity(
533
+ rule.baseSeverity,
534
+ confidence,
535
+ snapshot.boundaryType || "HOC"
536
+ ),
537
+ reason: interpolateMessage(rule.messageTemplate, {
538
+ confidence: (confidence * 100).toFixed(0),
539
+ }),
540
+ });
541
+ }
542
+ }
543
+ }
544
+
545
+ record(snapshot);
546
+ return issues;
547
+ }
548
+
549
+
550
+ /* -------------------------------
551
+ Worker Message Handler
552
+ -------------------------------- */
553
+
554
+ self.onmessage = (e) => {
555
+ const { type, payload } = e.data;
556
+
557
+ /* Init rules once */
558
+ if (type === "INIT_RULES") {
559
+ RULES = payload;
560
+ // console.log('[PerfGuard Worker] Rules loaded:', RULES.length);
561
+ self.postMessage({ type: "INIT_SUCCESS", count: RULES.length });
562
+ return;
563
+ }
564
+
565
+ /* Evaluate snapshots */
566
+ if (type === "EVALUATE") {
567
+ const results = [];
568
+ let hasCritical = false;
569
+
570
+ for (const snapshot of payload) {
571
+ const issues = evaluate(snapshot);
572
+
573
+ if (issues.length) {
574
+ const componentResult = {
575
+ component: snapshot.component,
576
+ boundaryType: snapshot.boundaryType || "HOC",
577
+ metrics: {
578
+ renders: snapshot.renders,
579
+ avgTime: snapshot.avgTime,
580
+ maxTime: snapshot.maxTime,
581
+ },
582
+ issues,
583
+ };
584
+
585
+ // Check for critical issues
586
+ if (issues.some(i => i.severity === 'CRITICAL')) {
587
+ hasCritical = true;
588
+ componentResult.hasCritical = true;
589
+ }
590
+
591
+ results.push(componentResult);
592
+ }
593
+ }
594
+
595
+ // console.log('[PerfGuard Worker] Evaluation complete:', results.length, 'issues');
596
+ self.postMessage({
597
+ type: "RESULTS",
598
+ data: results,
599
+ hasCritical,
600
+ timestamp: Date.now()
601
+ });
602
+ }
603
+
604
+ /* Reset history */
605
+ if (type === "RESET") {
606
+ history.clear();
607
+ self.postMessage({ type: "RESET_SUCCESS" });
608
+ }
609
+
610
+ /* Get stats */
611
+ if (type === "GET_STATS") {
612
+ const stats = {
613
+ componentsTracked: history.size,
614
+ totalSnapshots: Array.from(history.values()).reduce((sum, list) => sum + list.length, 0),
615
+ rulesLoaded: RULES.length,
616
+ };
617
+ self.postMessage({ type: "STATS", data: stats });
618
+ }
619
+ };
620
+ `;
621
+ return new Worker(
622
+ URL.createObjectURL(new Blob([code], { type: "application/javascript" }))
623
+ );
624
+ }
625
+
626
+ // src/env.ts
627
+ var isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
628
+
629
+ // src/pref-engine/rules.ts
630
+ var PERF_RULES = [
631
+ // ========================================
632
+ // CRITICAL PERFORMANCE RULES
633
+ // ========================================
634
+ {
635
+ id: "BLOCKING_RENDER",
636
+ category: "PERFORMANCE",
637
+ baseSeverity: "CRITICAL",
638
+ predicate: {
639
+ field: "avgTime",
640
+ operator: ">",
641
+ value: 100
642
+ },
643
+ confidenceThreshold: 0.4,
644
+ messageTemplate: "CRITICAL: Component blocks UI thread for >100ms in {confidence}% of renders",
645
+ messageFields: ["confidence"],
646
+ docUrl: "https://web.dev/rail/"
647
+ },
648
+ {
649
+ id: "SLOW_RENDER",
650
+ category: "PERFORMANCE",
651
+ baseSeverity: "HIGH",
652
+ predicate: {
653
+ field: "avgTime",
654
+ operator: ">",
655
+ value: 16
656
+ },
657
+ confidenceThreshold: 0.6,
658
+ messageTemplate: "Average render time exceeded 16ms in {confidence}% of recent batches",
659
+ messageFields: ["confidence"]
660
+ },
661
+ {
662
+ id: "VERY_SLOW_RENDER",
663
+ category: "PERFORMANCE",
664
+ baseSeverity: "HIGH",
665
+ predicate: {
666
+ field: "avgTime",
667
+ operator: ">",
668
+ value: 50
669
+ },
670
+ confidenceThreshold: 0.5,
671
+ messageTemplate: "Critical: render time exceeded 50ms in {confidence}% of batches",
672
+ messageFields: ["confidence"]
673
+ },
674
+ {
675
+ id: "INCONSISTENT_PERFORMANCE",
676
+ category: "STABILITY",
677
+ baseSeverity: "MEDIUM",
678
+ predicate: {
679
+ field: "maxTime",
680
+ operator: ">",
681
+ value: 50
682
+ },
683
+ confidenceThreshold: 0.3,
684
+ messageTemplate: "Unstable performance: max render time spikes to >50ms",
685
+ messageFields: ["confidence"]
686
+ },
687
+ // ========================================
688
+ // RENDER OPTIMIZATION RULES
689
+ // ========================================
690
+ {
691
+ id: "EXCESSIVE_RENDERS",
692
+ category: "PERFORMANCE",
693
+ baseSeverity: "MEDIUM",
694
+ predicate: {
695
+ field: "renders",
696
+ operator: ">",
697
+ value: 20
698
+ },
699
+ confidenceThreshold: 0.6,
700
+ messageTemplate: "Component rendered excessively in {confidence}% of recent batches",
701
+ messageFields: ["confidence"]
702
+ },
703
+ {
704
+ id: "RENDER_THRASHING",
705
+ category: "PERFORMANCE",
706
+ baseSeverity: "HIGH",
707
+ predicate: {
708
+ field: "renders",
709
+ operator: ">",
710
+ value: 50
711
+ },
712
+ confidenceThreshold: 0.4,
713
+ messageTemplate: "Severe render thrashing detected: >50 renders in {confidence}% of batches",
714
+ messageFields: ["confidence"]
715
+ },
716
+ {
717
+ id: "SUSPICIOUS_RENDER_LOOP",
718
+ category: "RELIABILITY",
719
+ baseSeverity: "CRITICAL",
720
+ predicate: {
721
+ field: "renders",
722
+ operator: ">",
723
+ value: 100
724
+ },
725
+ confidenceThreshold: 0.2,
726
+ messageTemplate: "Potential infinite render loop: >100 renders detected",
727
+ messageFields: ["confidence"]
728
+ },
729
+ // ========================================
730
+ // REGRESSION DETECTION
731
+ // ========================================
732
+ {
733
+ id: "PERF_REGRESSION",
734
+ category: "PERFORMANCE",
735
+ baseSeverity: "HIGH",
736
+ regression: {
737
+ field: "avgTime",
738
+ multiplier: 1.3
739
+ },
740
+ messageTemplate: "Avg render time increased from {prevValue}ms to {currValue}ms",
741
+ messageFields: ["prevValue", "currValue"]
742
+ },
743
+ {
744
+ id: "SEVERE_PERF_REGRESSION",
745
+ category: "PERFORMANCE",
746
+ baseSeverity: "CRITICAL",
747
+ regression: {
748
+ field: "avgTime",
749
+ multiplier: 2
750
+ },
751
+ messageTemplate: "SEVERE: Performance degraded by 2x from {prevValue}ms to {currValue}ms",
752
+ messageFields: ["prevValue", "currValue"]
753
+ },
754
+ {
755
+ id: "RENDER_COUNT_SPIKE",
756
+ category: "STABILITY",
757
+ baseSeverity: "MEDIUM",
758
+ regression: {
759
+ field: "renders",
760
+ multiplier: 1.5
761
+ },
762
+ messageTemplate: "Render count increased from {prevValue} to {currValue}",
763
+ messageFields: ["prevValue", "currValue"]
764
+ },
765
+ {
766
+ id: "MAX_TIME_REGRESSION",
767
+ category: "STABILITY",
768
+ baseSeverity: "HIGH",
769
+ regression: {
770
+ field: "maxTime",
771
+ multiplier: 1.5
772
+ },
773
+ messageTemplate: "Peak render time degraded from {prevValue}ms to {currValue}ms",
774
+ messageFields: ["prevValue", "currValue"]
775
+ },
776
+ // ========================================
777
+ // USER EXPERIENCE RULES
778
+ // ========================================
779
+ {
780
+ id: "JANKY_ANIMATION",
781
+ category: "UX",
782
+ baseSeverity: "MEDIUM",
783
+ predicate: {
784
+ field: "avgTime",
785
+ operator: ">",
786
+ value: 16.67
787
+ // 60fps threshold
788
+ },
789
+ confidenceThreshold: 0.7,
790
+ messageTemplate: "Janky animations: unable to maintain 60fps in {confidence}% of renders",
791
+ messageFields: ["confidence"]
792
+ },
793
+ {
794
+ id: "POOR_INTERACTION_RESPONSE",
795
+ category: "UX",
796
+ baseSeverity: "HIGH",
797
+ predicate: {
798
+ field: "avgTime",
799
+ operator: ">",
800
+ value: 50
801
+ },
802
+ confidenceThreshold: 0.5,
803
+ messageTemplate: "Poor interaction response: >50ms delay in {confidence}% of renders",
804
+ messageFields: ["confidence"]
805
+ },
806
+ // ========================================
807
+ // MEMORY & LEAK DETECTION
808
+ // ========================================
809
+ {
810
+ id: "RENDER_TIME_CREEP",
811
+ category: "MEMORY",
812
+ baseSeverity: "MEDIUM",
813
+ trend: {
814
+ field: "avgTime",
815
+ direction: "increasing",
816
+ threshold: 20
817
+ // 20% increase over time
818
+ },
819
+ messageTemplate: "Gradual performance degradation: render time increasing over time",
820
+ confidenceThreshold: 0.6
821
+ },
822
+ {
823
+ id: "RENDER_COUNT_CREEP",
824
+ category: "MEMORY",
825
+ baseSeverity: "MEDIUM",
826
+ trend: {
827
+ field: "renders",
828
+ direction: "increasing",
829
+ threshold: 30
830
+ },
831
+ messageTemplate: "Render count steadily increasing - possible state management issue",
832
+ confidenceThreshold: 0.6
833
+ },
834
+ // ========================================
835
+ // STABILITY & RELIABILITY
836
+ // ========================================
837
+ {
838
+ id: "ERRATIC_PERFORMANCE",
839
+ category: "STABILITY",
840
+ baseSeverity: "MEDIUM",
841
+ predicate: {
842
+ field: "maxTime",
843
+ operator: ">",
844
+ value: 100
845
+ },
846
+ confidenceThreshold: 0.3,
847
+ messageTemplate: "Erratic performance spikes detected (>100ms)",
848
+ messageFields: ["confidence"]
849
+ },
850
+ {
851
+ id: "FIRST_RENDER_SLOW",
852
+ category: "UX",
853
+ baseSeverity: "MEDIUM",
854
+ predicate: {
855
+ field: "maxTime",
856
+ operator: ">",
857
+ value: 200
858
+ },
859
+ confidenceThreshold: 0.8,
860
+ messageTemplate: "Slow initial mount: first render >200ms",
861
+ messageFields: ["confidence"]
862
+ },
863
+ // ========================================
864
+ // PRODUCTION READINESS CHECKS
865
+ // ========================================
866
+ {
867
+ id: "PROD_READY_PERF",
868
+ category: "PERFORMANCE",
869
+ baseSeverity: "INFO",
870
+ predicate: {
871
+ field: "avgTime",
872
+ operator: "<",
873
+ value: 10
874
+ },
875
+ confidenceThreshold: 0.9,
876
+ messageTemplate: "\u2705 Component meets production performance standards (<10ms avg)",
877
+ messageFields: ["confidence"]
878
+ },
879
+ // ========================================
880
+ // HELPFUL HINTS (DEVELOPMENT)
881
+ // ========================================
882
+ {
883
+ id: "DEV_HINT_MEMOIZATION",
884
+ category: "PERFORMANCE",
885
+ baseSeverity: "INFO",
886
+ predicate: {
887
+ field: "renders",
888
+ operator: ">",
889
+ value: 15
890
+ },
891
+ confidenceThreshold: 0.5,
892
+ messageTemplate: "\u{1F4A1} Consider React.memo or useMemo for this component",
893
+ messageFields: ["confidence"]
894
+ },
895
+ {
896
+ id: "DEV_HINT_OPTIMIZATION",
897
+ category: "PERFORMANCE",
898
+ baseSeverity: "INFO",
899
+ predicate: {
900
+ field: "avgTime",
901
+ operator: ">",
902
+ value: 20
903
+ },
904
+ confidenceThreshold: 0.5,
905
+ messageTemplate: "\u{1F4A1} Component could benefit from optimization (>20ms avg)",
906
+ messageFields: ["confidence"]
907
+ }
908
+ ];
909
+ function getRulesConfig() {
910
+ return PERF_RULES;
911
+ }
912
+ var RULE_GROUPS = {
913
+ CRITICAL_ONLY: PERF_RULES.filter((r) => r.baseSeverity === "CRITICAL"),
914
+ PERFORMANCE: PERF_RULES.filter((r) => r.category === "PERFORMANCE"),
915
+ UX: PERF_RULES.filter((r) => r.category === "UX"),
916
+ STABILITY: PERF_RULES.filter((r) => r.category === "STABILITY"),
917
+ MEMORY: PERF_RULES.filter((r) => r.category === "MEMORY"),
918
+ HINTS: PERF_RULES.filter((r) => r.id.startsWith("DEV_HINT") || r.id.startsWith("PROD_READY"))
919
+ };
920
+
921
+ // src/PrevGuardPanel.tsx
922
+ import { useEffect, useState } from "react";
923
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
924
+ function PerfGuardPanel() {
925
+ const [issues, setIssues] = useState([]);
926
+ const [open, setOpen] = useState(false);
927
+ const [selectedIssue, setSelectedIssue] = useState(null);
928
+ useEffect(() => {
929
+ subscribe(setIssues);
930
+ }, []);
931
+ if (!issues.length) {
932
+ return /* @__PURE__ */ jsx("div", { style: panel, children: /* @__PURE__ */ jsxs("div", { style: header, children: [
933
+ /* @__PURE__ */ jsx("div", { style: headerTitle, children: "PerfGuard" }),
934
+ /* @__PURE__ */ jsx("div", { style: headerSubtitle, children: "No issues detected" })
935
+ ] }) });
936
+ }
937
+ const criticalCount = issues.filter(
938
+ (i) => i.severity === "CRITICAL" && i.status === "ACTIVE"
939
+ ).length;
940
+ const highCount = issues.filter(
941
+ (i) => i.severity === "HIGH" && i.status === "ACTIVE"
942
+ ).length;
943
+ const activeCount = issues.filter(
944
+ (i) => i.status === "ACTIVE" || i.status === "NEW"
945
+ ).length;
946
+ const getSeverityClass = (severity) => {
947
+ switch (severity) {
948
+ case "CRITICAL":
949
+ return "severity-critical";
950
+ case "HIGH":
951
+ return "severity-high";
952
+ case "MEDIUM":
953
+ return "severity-medium";
954
+ default:
955
+ return "severity-low";
956
+ }
957
+ };
958
+ const sortedIssues = [...issues].sort((a, b) => {
959
+ if (a.status === b.status) return 0;
960
+ return a.status === "ACTIVE" ? -1 : 1;
961
+ });
962
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
963
+ /* @__PURE__ */ jsx("style", { children: styles }),
964
+ /* @__PURE__ */ jsxs("div", { style: panel, children: [
965
+ /* @__PURE__ */ jsxs(
966
+ "div",
967
+ {
968
+ style: header,
969
+ onClick: () => setOpen((o) => !o),
970
+ className: "perfguard-header",
971
+ children: [
972
+ /* @__PURE__ */ jsxs("div", { style: headerLeft, children: [
973
+ /* @__PURE__ */ jsxs("div", { style: iconWrapper, children: [
974
+ /* @__PURE__ */ jsx("span", { style: icon, children: "\u26A1" }),
975
+ criticalCount > 0 && /* @__PURE__ */ jsx("span", { style: criticalPing })
976
+ ] }),
977
+ /* @__PURE__ */ jsxs("div", { children: [
978
+ /* @__PURE__ */ jsxs("div", { style: headerTitle, children: [
979
+ "PerfGuard",
980
+ /* @__PURE__ */ jsx("span", { style: betaBadge, children: "BETA" })
981
+ ] }),
982
+ /* @__PURE__ */ jsxs("div", { style: headerSubtitle, children: [
983
+ activeCount,
984
+ " active issue",
985
+ activeCount !== 1 ? "s" : ""
986
+ ] })
987
+ ] })
988
+ ] }),
989
+ /* @__PURE__ */ jsxs("div", { style: headerRight, children: [
990
+ criticalCount > 0 && /* @__PURE__ */ jsxs("span", { style: criticalBadge, className: "critical-badge", children: [
991
+ criticalCount,
992
+ " Critical"
993
+ ] }),
994
+ highCount > 0 && /* @__PURE__ */ jsxs("span", { style: highBadge, children: [
995
+ highCount,
996
+ " High"
997
+ ] }),
998
+ /* @__PURE__ */ jsx(
999
+ "svg",
1000
+ {
1001
+ style: {
1002
+ ...chevron,
1003
+ transform: open ? "rotate(180deg)" : "rotate(0deg)"
1004
+ },
1005
+ fill: "none",
1006
+ viewBox: "0 0 24 24",
1007
+ stroke: "currentColor",
1008
+ children: /* @__PURE__ */ jsx(
1009
+ "path",
1010
+ {
1011
+ strokeLinecap: "round",
1012
+ strokeLinejoin: "round",
1013
+ strokeWidth: 2,
1014
+ d: "M19 9l-7 7-7-7"
1015
+ }
1016
+ )
1017
+ }
1018
+ )
1019
+ ] })
1020
+ ]
1021
+ }
1022
+ ),
1023
+ open && /* @__PURE__ */ jsxs("div", { style: issuesContainer, className: "issues-container", children: [
1024
+ /* @__PURE__ */ jsx("div", { style: issuesList, children: sortedIssues.map((issue, idx) => /* @__PURE__ */ jsx(
1025
+ "div",
1026
+ {
1027
+ onClick: () => setSelectedIssue(
1028
+ selectedIssue === issue.id ? null : issue.id
1029
+ ),
1030
+ style: issueCard(idx),
1031
+ className: `issue-card ${getSeverityClass(issue.severity)}`,
1032
+ children: /* @__PURE__ */ jsxs("div", { style: issueContent, children: [
1033
+ /* @__PURE__ */ jsxs("div", { style: issueMain, children: [
1034
+ /* @__PURE__ */ jsxs("div", { style: issueHeader, children: [
1035
+ /* @__PURE__ */ jsx(
1036
+ "span",
1037
+ {
1038
+ style: statusDot(issue.status),
1039
+ className: issue.status === "ACTIVE" ? "status-dot-pulse" : ""
1040
+ }
1041
+ ),
1042
+ /* @__PURE__ */ jsx("span", { style: componentName, children: issue.component }),
1043
+ issue.status === "RESOLVED" ? /* @__PURE__ */ jsx("span", { style: statusBadgeResolved, children: "\u2713 Resolved" }) : /* @__PURE__ */ jsx(
1044
+ "span",
1045
+ {
1046
+ style: statusBadgeOpen,
1047
+ className: "status-badge-open",
1048
+ children: "\u25CF Open"
1049
+ }
1050
+ )
1051
+ ] }),
1052
+ /* @__PURE__ */ jsx("div", { style: ruleId, children: issue.ruleId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ") }),
1053
+ /* @__PURE__ */ jsxs("div", { style: issueMetrics, children: [
1054
+ /* @__PURE__ */ jsxs("div", { style: metric, children: [
1055
+ /* @__PURE__ */ jsx("span", { style: metricLabel, children: "Severity:" }),
1056
+ /* @__PURE__ */ jsx("span", { style: metricValue, children: issue.severity })
1057
+ ] }),
1058
+ /* @__PURE__ */ jsxs("div", { style: metric, children: [
1059
+ /* @__PURE__ */ jsx("span", { style: metricLabel, children: "Confidence:" }),
1060
+ /* @__PURE__ */ jsxs("div", { style: confidenceWrapper, children: [
1061
+ /* @__PURE__ */ jsx("div", { style: confidenceBar, children: /* @__PURE__ */ jsx(
1062
+ "div",
1063
+ {
1064
+ style: {
1065
+ ...confidenceProgress,
1066
+ width: `${issue.confidence * 100}%`
1067
+ },
1068
+ className: `confidence-progress ${getSeverityClass(
1069
+ issue.severity
1070
+ )}`
1071
+ }
1072
+ ) }),
1073
+ /* @__PURE__ */ jsxs("span", { style: confidenceValue, children: [
1074
+ Math.round(issue.confidence * 100),
1075
+ "%"
1076
+ ] })
1077
+ ] })
1078
+ ] })
1079
+ ] }),
1080
+ selectedIssue === issue.id && /* @__PURE__ */ jsx("div", { style: issueDetails, className: "issue-details", children: /* @__PURE__ */ jsxs("div", { style: detailsContent, children: [
1081
+ /* @__PURE__ */ jsxs("span", { style: detailsText, children: [
1082
+ "Rule ID: ",
1083
+ issue.ruleId
1084
+ ] }),
1085
+ /* @__PURE__ */ jsx("span", { style: detailsDescription, children: issue.reason })
1086
+ ] }) })
1087
+ ] }),
1088
+ /* @__PURE__ */ jsx(
1089
+ "svg",
1090
+ {
1091
+ style: {
1092
+ ...expandIcon,
1093
+ transform: selectedIssue === issue.id ? "rotate(180deg)" : "rotate(0deg)"
1094
+ },
1095
+ fill: "none",
1096
+ viewBox: "0 0 24 24",
1097
+ stroke: "currentColor",
1098
+ children: /* @__PURE__ */ jsx(
1099
+ "path",
1100
+ {
1101
+ strokeLinecap: "round",
1102
+ strokeLinejoin: "round",
1103
+ strokeWidth: 2,
1104
+ d: "M19 9l-7 7-7-7"
1105
+ }
1106
+ )
1107
+ }
1108
+ )
1109
+ ] })
1110
+ },
1111
+ issue.id
1112
+ )) }),
1113
+ /* @__PURE__ */ jsxs("div", { style: footer, children: [
1114
+ /* @__PURE__ */ jsx("span", { style: footerText, children: "Last scan: just now" }),
1115
+ /* @__PURE__ */ jsx("button", { style: scanButton, children: "Run Scan" })
1116
+ ] })
1117
+ ] })
1118
+ ] })
1119
+ ] });
1120
+ }
1121
+ var styles = `
1122
+ @keyframes slideUp {
1123
+ from { transform: translateY(20px); opacity: 0; }
1124
+ to { transform: translateY(0); opacity: 1; }
1125
+ }
1126
+
1127
+ @keyframes slideDown {
1128
+ from { max-height: 0; opacity: 0; }
1129
+ to { max-height: 600px; opacity: 1; }
1130
+ }
1131
+
1132
+ @keyframes pulseGlow {
1133
+ 0%, 100% { box-shadow: 0 0 15px rgba(239, 68, 68, 0.3); }
1134
+ 50% { box-shadow: 0 0 25px rgba(239, 68, 68, 0.6); }
1135
+ }
1136
+
1137
+ @keyframes ping {
1138
+ 75%, 100% {
1139
+ transform: scale(2);
1140
+ opacity: 0;
1141
+ }
1142
+ }
1143
+
1144
+ @keyframes pulse {
1145
+ 0%, 100% { opacity: 1; }
1146
+ 50% { opacity: 0.5; }
1147
+ }
1148
+
1149
+ .perfguard-header:hover {
1150
+ background: linear-gradient(to right, #1e293b, #0f172a);
1151
+ }
1152
+
1153
+ .critical-badge {
1154
+ animation: pulseGlow 2s ease-in-out infinite;
1155
+ }
1156
+
1157
+ .status-dot-pulse {
1158
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1159
+ }
1160
+
1161
+ .status-badge-open {
1162
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1163
+ }
1164
+
1165
+ .issues-container {
1166
+ animation: slideDown 0.3s ease-out;
1167
+ }
1168
+
1169
+ .issue-card {
1170
+ animation: slideUp 0.3s ease-out;
1171
+ transition: all 0.2s ease;
1172
+ }
1173
+
1174
+ .issue-card:hover {
1175
+ background-color: rgba(30, 41, 59, 0.5);
1176
+ }
1177
+
1178
+ .issue-card.severity-critical {
1179
+ border-left: 4px solid rgba(239, 68, 68, 0.3);
1180
+ background-color: rgba(239, 68, 68, 0.1);
1181
+ }
1182
+
1183
+ .issue-card.severity-high {
1184
+ border-left: 4px solid rgba(249, 115, 22, 0.3);
1185
+ background-color: rgba(249, 115, 22, 0.1);
1186
+ }
1187
+
1188
+ .issue-card.severity-medium {
1189
+ border-left: 4px solid rgba(234, 179, 8, 0.3);
1190
+ background-color: rgba(234, 179, 8, 0.1);
1191
+ }
1192
+
1193
+ .issue-card.severity-low {
1194
+ border-left: 4px solid rgba(59, 130, 246, 0.3);
1195
+ background-color: rgba(59, 130, 246, 0.1);
1196
+ }
1197
+
1198
+ .issue-details {
1199
+ animation: slideDown 0.2s ease-out;
1200
+ }
1201
+
1202
+ .confidence-progress {
1203
+ transition: width 0.5s ease-out;
1204
+ }
1205
+
1206
+ .confidence-progress.severity-critical {
1207
+ background-color: #ef4444;
1208
+ }
1209
+
1210
+ .confidence-progress.severity-high {
1211
+ background-color: #f97316;
1212
+ }
1213
+
1214
+ .confidence-progress.severity-medium {
1215
+ background-color: #eab308;
1216
+ }
1217
+
1218
+ .confidence-progress.severity-low {
1219
+ background-color: #3b82f6;
1220
+ }
1221
+ `;
1222
+ var panel = {
1223
+ position: "fixed",
1224
+ bottom: 16,
1225
+ right: 16,
1226
+ width: 480,
1227
+ fontFamily: "monospace",
1228
+ fontSize: 14,
1229
+ zIndex: 1e4,
1230
+ background: "#0f172a",
1231
+ borderRadius: 8,
1232
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
1233
+ border: "1px solid #334155",
1234
+ overflow: "hidden",
1235
+ animation: "slideUp 0.3s ease-out"
1236
+ };
1237
+ var header = {
1238
+ padding: "12px 16px",
1239
+ background: "linear-gradient(to right, #1e293b, #0f172a)",
1240
+ borderBottom: "1px solid #334155",
1241
+ cursor: "pointer",
1242
+ display: "flex",
1243
+ alignItems: "center",
1244
+ justifyContent: "space-between",
1245
+ transition: "background 0.2s ease"
1246
+ };
1247
+ var headerLeft = {
1248
+ display: "flex",
1249
+ alignItems: "center",
1250
+ gap: 12
1251
+ };
1252
+ var iconWrapper = {
1253
+ position: "relative"
1254
+ };
1255
+ var icon = {
1256
+ fontSize: 24
1257
+ };
1258
+ var criticalPing = {
1259
+ position: "absolute",
1260
+ top: -4,
1261
+ right: -4,
1262
+ width: 12,
1263
+ height: 12,
1264
+ background: "#ef4444",
1265
+ borderRadius: "50%",
1266
+ animation: "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite"
1267
+ };
1268
+ var headerTitle = {
1269
+ fontWeight: "bold",
1270
+ color: "#ffffff",
1271
+ display: "flex",
1272
+ alignItems: "center",
1273
+ gap: 8
1274
+ };
1275
+ var betaBadge = {
1276
+ fontSize: 10,
1277
+ padding: "2px 8px",
1278
+ background: "rgba(59, 130, 246, 0.2)",
1279
+ color: "#60a5fa",
1280
+ borderRadius: 4,
1281
+ border: "1px solid rgba(59, 130, 246, 0.3)"
1282
+ };
1283
+ var headerSubtitle = {
1284
+ fontSize: 12,
1285
+ color: "#94a3b8"
1286
+ };
1287
+ var headerRight = {
1288
+ display: "flex",
1289
+ alignItems: "center",
1290
+ gap: 8
1291
+ };
1292
+ var criticalBadge = {
1293
+ padding: "4px 8px",
1294
+ fontSize: 12,
1295
+ background: "rgba(239, 68, 68, 0.2)",
1296
+ color: "#f87171",
1297
+ borderRadius: 4,
1298
+ border: "1px solid rgba(239, 68, 68, 0.3)"
1299
+ };
1300
+ var highBadge = {
1301
+ padding: "4px 8px",
1302
+ fontSize: 12,
1303
+ background: "rgba(249, 115, 22, 0.2)",
1304
+ color: "#fb923c",
1305
+ borderRadius: 4,
1306
+ border: "1px solid rgba(249, 115, 22, 0.3)"
1307
+ };
1308
+ var chevron = {
1309
+ width: 20,
1310
+ height: 20,
1311
+ color: "#94a3b8",
1312
+ transition: "transform 0.3s ease"
1313
+ };
1314
+ var issuesContainer = {
1315
+ maxHeight: 500,
1316
+ overflowY: "auto"
1317
+ };
1318
+ var issuesList = {
1319
+ borderTop: "1px solid #1e293b"
1320
+ };
1321
+ var issueCard = (idx) => ({
1322
+ padding: 16,
1323
+ cursor: "pointer",
1324
+ borderBottom: "1px solid #1e293b",
1325
+ animationDelay: `${idx * 50}ms`
1326
+ });
1327
+ var issueContent = {
1328
+ display: "flex",
1329
+ alignItems: "flex-start",
1330
+ justifyContent: "space-between",
1331
+ gap: 12
1332
+ };
1333
+ var issueMain = {
1334
+ flex: 1
1335
+ };
1336
+ var issueHeader = {
1337
+ display: "flex",
1338
+ alignItems: "center",
1339
+ gap: 8,
1340
+ marginBottom: 8
1341
+ };
1342
+ var statusDot = (status) => ({
1343
+ width: 8,
1344
+ height: 8,
1345
+ borderRadius: "50%",
1346
+ background: status === "OPEN" ? "#ef4444" : "#22c55e"
1347
+ });
1348
+ var componentName = {
1349
+ fontWeight: 600,
1350
+ color: "#ffffff"
1351
+ };
1352
+ var statusBadgeResolved = {
1353
+ padding: "2px 8px",
1354
+ fontSize: 12,
1355
+ borderRadius: 9999,
1356
+ background: "rgba(34, 197, 94, 0.2)",
1357
+ color: "#4ade80",
1358
+ border: "1px solid rgba(34, 197, 94, 0.3)"
1359
+ };
1360
+ var statusBadgeOpen = {
1361
+ padding: "2px 8px",
1362
+ fontSize: 12,
1363
+ borderRadius: 9999,
1364
+ background: "rgba(239, 68, 68, 0.2)",
1365
+ color: "#f87171",
1366
+ border: "1px solid rgba(239, 68, 68, 0.3)"
1367
+ };
1368
+ var ruleId = {
1369
+ fontSize: 14,
1370
+ color: "#cbd5e1",
1371
+ marginBottom: 8,
1372
+ fontWeight: 500
1373
+ };
1374
+ var issueMetrics = {
1375
+ display: "flex",
1376
+ alignItems: "center",
1377
+ gap: 16,
1378
+ fontSize: 12
1379
+ };
1380
+ var metric = {
1381
+ display: "flex",
1382
+ alignItems: "center",
1383
+ gap: 4
1384
+ };
1385
+ var metricLabel = {
1386
+ color: "#64748b"
1387
+ };
1388
+ var metricValue = {
1389
+ fontWeight: "bold",
1390
+ color: "#e2e8f0"
1391
+ };
1392
+ var confidenceWrapper = {
1393
+ display: "flex",
1394
+ alignItems: "center",
1395
+ gap: 4
1396
+ };
1397
+ var confidenceBar = {
1398
+ width: 64,
1399
+ height: 6,
1400
+ background: "#334155",
1401
+ borderRadius: 9999,
1402
+ overflow: "hidden"
1403
+ };
1404
+ var confidenceProgress = {
1405
+ height: "100%"
1406
+ };
1407
+ var confidenceValue = {
1408
+ color: "#cbd5e1",
1409
+ fontWeight: 500
1410
+ };
1411
+ var issueDetails = {
1412
+ marginTop: 12,
1413
+ paddingTop: 12,
1414
+ borderTop: "1px solid #334155"
1415
+ };
1416
+ var detailsContent = {
1417
+ display: "flex",
1418
+ flexDirection: "column",
1419
+ // justifyContent: "space-between",
1420
+ alignItems: "flex-start",
1421
+ gap: 2,
1422
+ fontSize: 12,
1423
+ color: "#94a3b8"
1424
+ };
1425
+ var detailsDescription = {
1426
+ color: "#b8b594ff"
1427
+ };
1428
+ var detailsText = {
1429
+ color: "#94a3b8"
1430
+ };
1431
+ var expandIcon = {
1432
+ width: 16,
1433
+ height: 16,
1434
+ color: "#64748b",
1435
+ transition: "transform 0.2s ease",
1436
+ flexShrink: 0,
1437
+ marginTop: 4
1438
+ };
1439
+ var footer = {
1440
+ padding: 12,
1441
+ background: "rgba(30, 41, 59, 0.5)",
1442
+ borderTop: "1px solid #334155",
1443
+ display: "flex",
1444
+ justifyContent: "space-between",
1445
+ alignItems: "center"
1446
+ };
1447
+ var footerText = {
1448
+ fontSize: 12,
1449
+ color: "#94a3b8"
1450
+ };
1451
+ var scanButton = {
1452
+ fontSize: 12,
1453
+ padding: "6px 12px",
1454
+ background: "#3b82f6",
1455
+ color: "#ffffff",
1456
+ border: "none",
1457
+ borderRadius: 4,
1458
+ cursor: "pointer",
1459
+ fontWeight: 500,
1460
+ fontFamily: "monospace",
1461
+ transition: "background 0.2s ease"
1462
+ };
1463
+
1464
+ // src/PerfProvider.tsx
1465
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1466
+ var worker = null;
1467
+ function PerfProvider({ children }) {
1468
+ const [stats, setStats] = useState2({ issues: 0, critical: 0 });
1469
+ if (!isDev) {
1470
+ return /* @__PURE__ */ jsx2(Fragment2, { children });
1471
+ }
1472
+ useEffect2(() => {
1473
+ try {
1474
+ worker = createAnalyzerWorker();
1475
+ const rules = getRulesConfig();
1476
+ worker.postMessage({
1477
+ type: "INIT_RULES",
1478
+ payload: rules
1479
+ });
1480
+ worker.onmessage = (e) => {
1481
+ const { type, data, hasCritical } = e.data;
1482
+ if (type === "INIT_SUCCESS") {
1483
+ console.log(`\u2705 [PerfGuard] Worker ready with ${e.data.count} rules`);
1484
+ }
1485
+ if (type === "RESULTS") {
1486
+ setStats((prev) => ({
1487
+ issues: prev.issues + data.length,
1488
+ critical: prev.critical + (hasCritical ? 1 : 0)
1489
+ }));
1490
+ data.forEach((result) => {
1491
+ showWarning(result);
1492
+ if (result.hasCritical) {
1493
+ showCriticalAlert(result);
1494
+ }
1495
+ });
1496
+ }
1497
+ if (type === "STATS") {
1498
+ console.log("\u{1F4CA} [PerfGuard] Stats:", e.data.data);
1499
+ }
1500
+ };
1501
+ worker.onerror = (err) => {
1502
+ console.error("[PerfGuard] Worker error:", err);
1503
+ };
1504
+ } catch (err) {
1505
+ console.warn("[PerfGuard] Worker failed to start", err);
1506
+ return;
1507
+ }
1508
+ const interval = setInterval(() => {
1509
+ const data = flushMetrics();
1510
+ if (data.length) {
1511
+ console.log(`[PerfGuard] Flushing ${data.length} snapshot(s)`);
1512
+ worker?.postMessage({
1513
+ type: "EVALUATE",
1514
+ payload: data
1515
+ });
1516
+ }
1517
+ }, 5e3);
1518
+ const statsInterval = setInterval(() => {
1519
+ worker?.postMessage({ type: "GET_STATS" });
1520
+ }, 3e4);
1521
+ return () => {
1522
+ clearInterval(interval);
1523
+ clearInterval(statsInterval);
1524
+ worker?.terminate();
1525
+ worker = null;
1526
+ };
1527
+ }, []);
1528
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
1529
+ children,
1530
+ /* @__PURE__ */ jsx2(PerfGuardPanel, {})
1531
+ ] });
1532
+ }
1533
+
1534
+ // src/withPerfGuard.tsx
1535
+ import { Profiler } from "react";
1536
+ import { jsx as jsx3 } from "react/jsx-runtime";
1537
+ var DEFAULT_OPTIONS = {
1538
+ boundaryType: "INLINE",
1539
+ enabled: true
1540
+ };
1541
+ function withPerfGuard(Component, options) {
1542
+ const { boundaryType, enabled } = {
1543
+ ...DEFAULT_OPTIONS,
1544
+ ...options
1545
+ };
1546
+ const fallbackId = "AnonymousComponent";
1547
+ const name = Component.displayName || Component.name || `PerfGuard(${fallbackId})`;
1548
+ if (process.env.NODE_ENV === "production" || !enabled) {
1549
+ return Component;
1550
+ }
1551
+ const Guarded = (props) => {
1552
+ return /* @__PURE__ */ jsx3(
1553
+ Profiler,
1554
+ {
1555
+ id: name,
1556
+ onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
1557
+ collectMetric({
1558
+ component: id,
1559
+ phase,
1560
+ actualDuration,
1561
+ baseDuration,
1562
+ startTime,
1563
+ commitTime,
1564
+ boundaryType
1565
+ });
1566
+ },
1567
+ children: /* @__PURE__ */ jsx3(Component, { ...props })
1568
+ }
1569
+ );
1570
+ };
1571
+ Guarded.displayName = `withPerfGuard(${name})`;
1572
+ return Guarded;
1573
+ }
1574
+
1575
+ // src/PerfProfiler.tsx
1576
+ import { Profiler as Profiler2 } from "react";
1577
+ import { Fragment as Fragment3, jsx as jsx4 } from "react/jsx-runtime";
1578
+ function PerfProfiler({
1579
+ id,
1580
+ children,
1581
+ boundaryType = "INLINE"
1582
+ }) {
1583
+ if (!isDev) {
1584
+ return /* @__PURE__ */ jsx4(Fragment3, { children });
1585
+ }
1586
+ return /* @__PURE__ */ jsx4(
1587
+ Profiler2,
1588
+ {
1589
+ id,
1590
+ onRender: (component, phase, actualDuration, baseDuration, startTime, commitTime) => {
1591
+ collectMetric({
1592
+ component,
1593
+ phase,
1594
+ actualDuration,
1595
+ baseDuration,
1596
+ startTime,
1597
+ commitTime,
1598
+ boundaryType
1599
+ });
1600
+ },
1601
+ children
1602
+ }
1603
+ );
1604
+ }
1605
+ export {
1606
+ PerfProfiler,
1607
+ PerfProvider,
1608
+ withPerfGuard
1609
+ };