-
Notifications
You must be signed in to change notification settings - Fork 71
Expand file tree
/
Copy pathindex.html
More file actions
4718 lines (4523 loc) · 347 KB
/
index.html
File metadata and controls
4718 lines (4523 loc) · 347 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodeFlow — Open Source Architecture Intelligence</title>
<meta name="description" content="Visualize any GitHub repository's architecture in seconds. See dependencies, blast radius, code ownership, security issues, and design patterns. No installation required.">
<meta name="keywords" content="code visualization, architecture, github, dependency graph, blast radius, code analysis, open source">
<meta name="author" content="CodeFlow">
<meta name="robots" content="index, follow">
<meta property="og:title" content="CodeFlow — Visualize Your Codebase Architecture">
<meta property="og:description" content="Turn any GitHub repo into an interactive architecture map in seconds. Zero setup, privacy-first, runs in your browser.">
<meta property="og:type" content="website">
<meta property="og:image" content="https://via.placeholder.com/1200x630?text=CodeFlow">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="CodeFlow — Visualize Your Codebase Architecture">
<meta name="twitter:description" content="Turn any GitHub repo into an interactive architecture map in seconds. Zero setup, privacy-first.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.5/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-sankey/0.12.3/d3-sankey.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/8.11.3/acorn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web-tree-sitter@0.25.10/tree-sitter.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/11.1.0/jsrsasign-all-min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg0:#0a0a0c;--bg1:#0f0f12;--bg2:#161619;--bg3:#1c1c21;--bg4:#252529;--hover:#2a2a30;--border:#2d2d35;--border2:#222228;--t0:#f0f0f2;--t1:#c8c8cd;--t2:#8b8b95;--t3:#5c5c66;--acc:#00ff9d;--acc2:#00cc7d;--accbg:rgba(0,255,157,0.08);--blue:#4d9fff;--purple:#a78bfa;--orange:#ff9f43;--red:#ff5f5f;--cyan:#22d3ee;--pink:#ec4899;--green:#22c55e}
.light{--bg0:#fff;--bg1:#f8f9fa;--bg2:#f1f3f4;--bg3:#e8eaed;--bg4:#dadce0;--hover:#e2e4e8;--border:#dadce0;--border2:#e8eaed;--t0:#1a1a1a;--t1:#3c3c3c;--t2:#6c6c6c;--t3:#9c9c9c;--acc:#00a86b;--acc2:#008f5b;--accbg:rgba(0,168,107,0.08)}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'JetBrains Mono',monospace;background:var(--bg0);color:var(--t0);height:100vh;overflow:hidden}
.app{display:flex;height:100vh;flex-direction:column}
.topbar{height:48px;background:var(--bg1);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 16px;gap:16px;flex-shrink:0}
.logo{display:flex;align-items:center;gap:10px;cursor:pointer}
.logo-mark{width:28px;height:28px;background:linear-gradient(135deg,var(--acc),var(--cyan));border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:14px}
.logo-text{font-size:14px;font-weight:700;color:var(--acc)}
.repo-input-group{display:flex;gap:8px;flex:1;max-width:500px}
.repo-input{flex:1;padding:6px 12px;background:var(--bg0);border:1px solid var(--border);border-radius:6px;color:var(--t0);font-family:inherit;font-size:11px}
.repo-input:focus{outline:none;border-color:var(--acc)}
.topbar-actions{display:flex;gap:6px;margin-left:auto}
.top-btn{padding:6px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--t1);font-family:inherit;font-size:10px;cursor:pointer;display:flex;align-items:center;gap:6px;white-space:nowrap}
.top-btn:hover{background:var(--bg4);border-color:var(--acc);color:var(--acc)}
.top-btn.primary{background:var(--acc);color:var(--bg0);border-color:var(--acc)}
.top-btn.primary:hover{background:var(--acc2)}
.top-btn:disabled{opacity:0.5;cursor:not-allowed}
.main{display:flex;flex:1;overflow:hidden}
.sidebar{width:260px;min-width:180px;max-width:400px;background:var(--bg1);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;position:relative}
.resize-handle{position:absolute;top:0;right:-4px;width:8px;height:100%;cursor:col-resize;z-index:10}
.resize-handle:hover{background:var(--acc);opacity:0.3}
.sidebar-section{padding:12px;border-bottom:1px solid var(--border)}
.sidebar-title{font-size:9px;font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
.sidebar-scroll{flex:1;overflow-y:auto;padding:8px}
.sidebar-scroll::-webkit-scrollbar{width:6px}
.sidebar-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.view-modes{display:flex;flex-direction:column;gap:2px}
.view-mode{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;cursor:pointer;font-size:11px;color:var(--t1)}
.view-mode:hover{background:var(--hover)}
.view-mode.active{background:var(--accbg);color:var(--acc)}
.view-mode-icon{font-size:14px}
.view-mode-badge{margin-left:auto;font-size:9px;background:var(--red);color:white;padding:2px 6px;border-radius:8px}
.stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:6px}
.stat-card{background:var(--bg0);border:1px solid var(--border2);border-radius:6px;padding:10px;text-align:center}
.stat-value{font-size:18px;font-weight:700;color:var(--acc)}
.stat-label{font-size:8px;color:var(--t3);text-transform:uppercase;margin-top:2px}
.stat-card.warn .stat-value{color:var(--orange)}
.health-score{display:flex;align-items:center;gap:12px;padding:12px;background:var(--bg0);border-radius:8px}
.health-ring{width:48px;height:48px;position:relative}
.health-ring svg{transform:rotate(-90deg)}
.health-ring-value{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700}
.health-info{flex:1}
.health-grade{font-size:16px;font-weight:700}
.health-label{font-size:9px;color:var(--t3)}
.tree-folder{display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:4px;cursor:pointer;font-size:11px;color:var(--t1)}
.tree-folder:hover{background:var(--hover)}
.tree-folder.filtered{background:var(--accbg);color:var(--acc)}
.tree-toggle{width:12px;font-size:8px;color:var(--t3);transition:transform 0.15s}
.tree-toggle.open{transform:rotate(90deg)}
.tree-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tree-count{font-size:9px;color:var(--t3);background:var(--bg3);padding:1px 5px;border-radius:8px}
.tree-children{margin-left:16px}
.tree-file{display:flex;align-items:center;gap:6px;padding:4px 8px;margin-left:18px;border-radius:4px;cursor:pointer;font-size:10px;color:var(--t2)}
.tree-file:hover{background:var(--hover);color:var(--t0)}
.tree-file.active{background:var(--accbg);color:var(--acc)}
.canvas-area{flex:1;position:relative;background:var(--bg0);overflow:hidden}
.canvas-area svg{width:100%;height:100%;display:block}
.canvas-toolbar{position:absolute;top:12px;left:12px;display:flex;gap:4px;z-index:50}
.canvas-info{position:absolute;bottom:12px;left:12px;display:flex;gap:8px;z-index:50}
.info-chip{padding:6px 10px;background:var(--bg1);border:1px solid var(--border);border-radius:6px;font-size:10px;color:var(--t2)}
.info-chip strong{color:var(--acc)}
.tool-btn{width:32px;height:32px;background:var(--bg1);border:1px solid var(--border);border-radius:6px;color:var(--t2);font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.tool-btn:hover{background:var(--bg2);color:var(--t0);border-color:var(--acc)}
.legend{position:absolute;top:12px;right:12px;background:var(--bg1);border:1px solid var(--border);border-radius:8px;z-index:50;min-width:140px;max-height:300px;overflow-y:auto;transition:all 0.2s}
.legend.collapsed{padding:8px;min-width:auto;max-height:none;overflow:hidden}
.legend.collapsed .legend-content{display:none}
.legend-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;cursor:pointer}
.legend-header:hover{background:var(--hover);border-radius:6px}
.legend-toggle{font-size:10px;color:var(--t3);transition:transform 0.2s}
.legend.collapsed .legend-toggle{transform:rotate(-90deg)}
.legend-content{padding:0 12px 12px}
.legend::-webkit-scrollbar{width:4px}
.legend::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
.legend-title{font-size:9px;font-weight:600;color:var(--t3);text-transform:uppercase;margin-bottom:8px}
.legend-item{display:flex;align-items:center;gap:8px;font-size:10px;color:var(--t2);margin-bottom:4px;cursor:pointer;padding:2px 4px;border-radius:4px}
.legend-item:hover{background:var(--hover)}
.legend-item.active{background:var(--accbg);color:var(--acc)}
.legend-color{width:12px;height:12px;border-radius:3px;flex-shrink:0}
.right-panel{width:360px;min-width:280px;max-width:500px;background:var(--bg1);border-left:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;position:relative}
.right-panel .resize-handle{right:auto;left:-4px}
.panel-tabs{display:flex;border-bottom:1px solid var(--border);background:var(--bg2)}
.panel-tab{flex:1;padding:8px;background:transparent;border:none;border-bottom:2px solid transparent;color:var(--t3);font-family:inherit;font-size:9px;font-weight:600;cursor:pointer}
.panel-tab:hover{color:var(--t1)}
.panel-tab.active{color:var(--acc);border-bottom-color:var(--acc)}
.panel-content{flex:1;overflow-y:auto;padding:12px}
.panel-content::-webkit-scrollbar{width:6px}
.panel-content::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.card{background:var(--bg0);border:1px solid var(--border2);border-radius:8px;margin-bottom:8px;overflow:hidden}
.card-header{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;cursor:pointer}
.card-header:hover{background:var(--hover)}
.card-title{font-size:11px;color:var(--t0);display:flex;align-items:center;gap:8px}
.card-toggle{font-size:8px;color:var(--t3);transition:transform 0.15s}
.card-toggle.open{transform:rotate(90deg)}
.card-body{padding:12px;border-top:1px solid var(--border2);background:var(--bg2)}
.badge{font-size:9px;font-weight:600;padding:2px 6px;border-radius:6px}
.badge-default{background:var(--bg3);color:var(--t2)}
.badge-success{background:rgba(34,197,94,0.2);color:var(--green)}
.badge-warning{background:rgba(255,159,67,0.2);color:var(--orange)}
.badge-danger{background:rgba(255,95,95,0.2);color:var(--red)}
.badge-info{background:rgba(77,159,255,0.2);color:var(--blue)}
.progress-bar{height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}
.progress-fill{height:100%;border-radius:3px}
.owner-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;margin:8px 0}
.owner-segment{height:100%}
.owner-list{display:flex;flex-direction:column;gap:4px}
.owner-item{display:flex;align-items:center;gap:8px;font-size:10px}
.owner-avatar{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:9px;color:white}
.owner-name{flex:1;color:var(--t1)}
.owner-percent{color:var(--t3)}
.pattern-item{padding:12px;background:var(--bg0);border-radius:8px;margin-bottom:8px;border-left:3px solid var(--green)}
.pattern-item.anti{border-color:var(--red)}
.pattern-header{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.pattern-icon{font-size:20px}
.pattern-name{font-size:12px;color:var(--t0);font-weight:600}
.pattern-desc{font-size:10px;color:var(--t2);margin-bottom:8px;line-height:1.4}
.pattern-files{font-size:9px;color:var(--t3)}
.pattern-file{display:inline-block;background:var(--bg3);padding:2px 6px;border-radius:4px;margin:2px}
.pattern-metrics{display:flex;gap:12px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border2)}
.pattern-metric{text-align:center}
.pattern-metric-value{font-size:14px;font-weight:600;color:var(--acc)}
.pattern-metric-label{font-size:8px;color:var(--t3);text-transform:uppercase}
.security-item{padding:12px;background:var(--bg0);border-radius:8px;margin-bottom:8px;border-left:3px solid;cursor:pointer}
.security-item:hover{background:var(--hover)}
.security-item.high{border-color:var(--red)}
.security-item.medium{border-color:var(--orange)}
.security-item.low{border-color:var(--blue)}
.security-header{display:flex;align-items:center;gap:8px;margin-bottom:6px}
.security-title{font-size:11px;color:var(--t0);font-weight:500}
.security-desc{font-size:10px;color:var(--t2);margin-bottom:6px;line-height:1.4}
.security-path{font-size:9px;color:var(--t3);font-family:monospace}
.security-line{font-size:9px;color:var(--acc);margin-top:4px}
.security-code{font-size:9px;background:var(--bg3);padding:6px 8px;border-radius:4px;margin-top:6px;font-family:monospace;color:var(--orange);overflow-x:auto;white-space:pre}
.fn-item{background:var(--bg0);border-radius:6px;margin-bottom:6px;overflow:hidden}
.fn-header{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer}
.fn-header:hover{background:var(--hover)}
.fn-name{color:var(--cyan);font-size:10px;font-family:monospace}
.fn-line{font-size:9px;color:var(--t3)}
.fn-code{font-size:9px;background:var(--bg3);padding:8px;font-family:monospace;color:var(--t1);overflow-x:auto;white-space:pre;max-height:150px;overflow-y:auto;border-top:1px solid var(--border2)}
.blast-detail{margin-top:8px;padding-top:8px;border-top:1px solid var(--border2)}
.blast-file{display:flex;align-items:center;gap:6px;font-size:9px;color:var(--t2);padding:3px 0}
.blast-file:hover{color:var(--acc);cursor:pointer}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000}
.modal{background:var(--bg1);border:1px solid var(--border);border-radius:12px;width:90%;max-width:500px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column}
.modal-header{padding:16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
.modal-title{font-size:14px;font-weight:600;color:var(--t0)}
.modal-close{background:none;border:none;color:var(--t3);font-size:18px;cursor:pointer}
.modal-body{flex:1;overflow-y:auto;padding:16px}
.modal-footer{padding:16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px}
.form-group{margin-bottom:12px}
.form-label{display:block;font-size:10px;color:var(--t2);margin-bottom:6px}
.form-input{width:100%;padding:8px 10px;background:var(--bg0);border:1px solid var(--border);border-radius:6px;color:var(--t0);font-family:inherit;font-size:11px}
.form-input:focus{outline:none;border-color:var(--acc)}
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center;padding:40px}
.empty-icon{font-size:48px;margin-bottom:16px;opacity:0.3}
.empty-title{font-size:16px;font-weight:600;color:var(--t1);margin-bottom:8px}
.empty-desc{font-size:12px;color:var(--t3);max-width:300px}
.loading{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}
.spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--acc);border-radius:50%;animation:spin 0.8s linear infinite;margin-bottom:16px}
@keyframes spin{to{transform:rotate(360deg)}}
.loading-text{font-size:12px;color:var(--t2)}
.loading-progress{font-size:11px;color:var(--acc);margin-top:6px}
.tooltip{position:fixed;background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:8px 12px;pointer-events:none;z-index:1000;max-width:220px}
.tooltip-title{font-size:11px;font-weight:600;color:var(--acc)}
.tooltip-content{font-size:10px;color:var(--t2);margin-top:4px;white-space:pre-line}
.export-options{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
.export-option{padding:16px;background:var(--bg0);border:1px solid var(--border);border-radius:8px;cursor:pointer;text-align:center}
.export-option:hover{border-color:var(--acc);background:var(--accbg)}
.export-option-icon{font-size:24px;margin-bottom:8px}
.export-option-label{font-size:11px;color:var(--t1)}
.pr-header{padding:12px;background:var(--bg0);border-radius:8px;margin-bottom:12px}
.pr-title{font-size:12px;color:var(--t0);font-weight:500}
.pr-stats{display:flex;gap:12px;margin-top:8px;font-size:11px}
.pr-add{color:var(--green)}
.pr-del{color:var(--red)}
.pr-file{display:flex;align-items:center;gap:8px;padding:8px;background:var(--bg0);border-radius:6px;margin-bottom:4px}
.pr-file-name{flex:1;font-size:10px;color:var(--t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pr-file-impact{font-size:9px;padding:2px 6px;border-radius:4px}
.panel-header{padding:12px;border-bottom:1px solid var(--border);background:var(--bg2)}
.panel-title{font-size:12px;font-weight:600;color:var(--acc)}
.panel-subtitle{font-size:10px;color:var(--t3);margin-top:4px}
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);padding:12px 24px;border-radius:8px;font-size:12px;z-index:1001;animation:slideUp 0.3s ease}
.toast.success{background:var(--green);color:white}
.toast.error{background:var(--red);color:white}
.toast.warning{background:var(--orange);color:var(--bg0)}
@keyframes slideUp{from{opacity:0;transform:translateX(-50%) translateY(20px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
.reset-btn{background:transparent;border:1px solid var(--border);color:var(--t2);padding:4px 8px;border-radius:4px;font-size:9px;cursor:pointer;margin-left:4px}
.reset-btn:hover{border-color:var(--red);color:var(--red)}
.refresh-btn{background:transparent;border:1px solid var(--border);color:var(--t2);padding:4px 8px;border-radius:4px;font-size:9px;cursor:pointer;margin-left:8px;display:flex;align-items:center;gap:4px}
.refresh-btn:hover{border-color:var(--acc);color:var(--acc)}
.loading-owner{display:flex;align-items:center;gap:8px;padding:12px;color:var(--t2);font-size:10px}
.loading-owner::before{content:'';width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--acc);border-radius:50%;animation:spin 0.6s linear infinite}
.privacy-modal{max-width:450px}
.privacy-item{display:flex;align-items:flex-start;gap:10px;padding:10px;background:var(--bg0);border-radius:8px;margin-bottom:8px}
.privacy-icon{font-size:20px;flex-shrink:0}
.privacy-text{font-size:11px;color:var(--t1);line-height:1.5}
.privacy-title{font-weight:600;color:var(--t0);margin-bottom:4px}
.fn-callers{margin-top:8px;padding:8px;background:var(--bg0);border-radius:6px;border:1px solid var(--border)}
.fn-callers-title{font-size:9px;font-weight:600;color:var(--t3);text-transform:uppercase;margin-bottom:6px}
.fn-caller{display:flex;align-items:center;gap:6px;padding:4px 6px;border-radius:4px;font-size:10px;color:var(--t2);cursor:pointer}
.fn-caller:hover{background:var(--hover);color:var(--acc)}
.graph-config{position:absolute;top:12px;left:180px;background:var(--bg1);border:1px solid var(--border);border-radius:8px;padding:12px;z-index:50;width:220px}
.graph-config-title{font-size:9px;font-weight:600;color:var(--t3);text-transform:uppercase;margin-bottom:10px}
.config-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.config-label{font-size:10px;color:var(--t2);min-width:60px}
.config-slider{flex:1;height:4px;-webkit-appearance:none;background:var(--bg3);border-radius:2px;outline:none}
.config-slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;background:var(--acc);border-radius:50%;cursor:pointer}
.config-value{font-size:9px;color:var(--t3);min-width:28px;text-align:right}
.view-toggle{display:flex;gap:4px;margin-bottom:10px}
.view-btn{flex:1;padding:6px 8px;background:var(--bg0);border:1px solid var(--border);border-radius:4px;font-size:9px;color:var(--t2);cursor:pointer;text-align:center}
.view-btn:hover{border-color:var(--acc)}
.view-btn.active{background:var(--accbg);border-color:var(--acc);color:var(--acc)}
.config-check{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--t2);cursor:pointer}
.config-check input{accent-color:var(--acc)}
.lang-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;margin:8px 0}
.lang-bar-segment{height:100%;transition:width 0.3s}
.lang-legend{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
.lang-item{display:flex;align-items:center;gap:4px;font-size:9px;color:var(--t2)}
.lang-dot{width:8px;height:8px;border-radius:2px}
.loc-stat{text-align:center;padding:8px;background:var(--bg0);border:1px solid var(--border2);border-radius:6px;margin-top:8px}
.loc-value{font-size:20px;font-weight:700;color:var(--acc)}
.loc-label{font-size:9px;color:var(--t3);text-transform:uppercase}
.unused-fn{background:var(--bg0);border:1px solid var(--border);border-radius:8px;margin-bottom:8px;overflow:hidden}
.unused-fn-header{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;cursor:pointer}
.unused-fn-header:hover{background:var(--hover)}
.unused-fn-name{font-weight:600;color:var(--t0);font-size:12px}
.unused-fn-meta{display:flex;align-items:center;gap:8px}
.unused-fn-lines{font-size:9px;color:var(--orange);background:rgba(255,159,67,0.15);padding:2px 6px;border-radius:4px}
.unused-fn-loc{font-size:9px;color:var(--t3)}
.unused-fn-file{font-size:9px;color:var(--blue);cursor:pointer;padding:2px 6px;background:var(--bg2);border-radius:4px}
.unused-fn-file:hover{background:var(--bg3)}
.unused-fn-preview{padding:0 12px 12px;border-top:1px solid var(--border);margin-top:8px}
.unused-fn-code{background:var(--bg1);border-radius:6px;padding:10px;font-size:10px;font-family:'JetBrains Mono',monospace;color:var(--t1);overflow-x:auto;white-space:pre;max-height:200px;overflow-y:auto;line-height:1.5}
.unused-fn-path{font-size:9px;color:var(--t3);margin-bottom:8px;display:flex;align-items:center;gap:4px}
.unused-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px;padding:12px;background:var(--bg0);border-radius:8px}
.unused-summary-item{text-align:center}
.unused-summary-value{font-size:16px;font-weight:700;color:var(--orange)}
.unused-summary-label{font-size:9px;color:var(--t3);text-transform:uppercase}
.pr-modal{max-width:900px;width:95vw}
.pr-risk{display:flex;align-items:center;gap:16px;padding:16px;background:var(--bg0);border-radius:12px;margin-bottom:16px}
.pr-risk-score{width:80px;height:80px;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;font-weight:700}
.pr-risk-value{font-size:28px}
.pr-risk-label{font-size:9px;text-transform:uppercase;opacity:0.8}
.pr-risk-details{flex:1}
.pr-risk-title{font-size:14px;font-weight:600;color:var(--t0);margin-bottom:4px}
.pr-risk-desc{font-size:11px;color:var(--t2);line-height:1.5}
.pr-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:16px}
.pr-card{background:var(--bg0);border:1px solid var(--border);border-radius:8px;padding:12px}
.pr-card-title{font-size:10px;font-weight:600;color:var(--t3);text-transform:uppercase;margin-bottom:8px;display:flex;align-items:center;gap:6px}
.pr-reviewer{display:flex;align-items:center;gap:8px;padding:6px;border-radius:6px;margin-bottom:4px}
.pr-reviewer:hover{background:var(--hover)}
.pr-reviewer-avatar{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:white}
.pr-reviewer-name{font-size:11px;color:var(--t1);flex:1}
.pr-reviewer-pct{font-size:10px;color:var(--t3)}
.pr-chain{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-bottom:8px}
.pr-chain-file{font-size:9px;padding:3px 8px;background:var(--bg2);border-radius:4px;color:var(--t2)}
.pr-chain-arrow{color:var(--t3);font-size:10px}
.pr-files-list{max-height:250px;overflow-y:auto}
.pr-file-row{display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;margin-bottom:4px;background:var(--bg0)}
.pr-file-row:hover{background:var(--hover)}
.pr-file-status{width:8px;height:8px;border-radius:50%}
.pr-file-info{flex:1;min-width:0}
.pr-file-path{font-size:11px;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pr-file-folder{font-size:9px;color:var(--t3)}
.pr-file-badges{display:flex;gap:4px}
.pr-mini-badge{font-size:8px;padding:2px 5px;border-radius:3px}
.treemap-container{width:100%;height:100%;position:relative}
.treemap-cell{position:absolute;overflow:hidden;border:1px solid var(--bg0);transition:all 0.2s;cursor:pointer}
.treemap-cell:hover{z-index:10;transform:scale(1.02);box-shadow:0 4px 12px rgba(0,0,0,0.3)}
.treemap-label{position:absolute;bottom:4px;left:4px;right:4px;font-size:9px;font-weight:500;color:white;text-shadow:0 1px 2px rgba(0,0,0,0.5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.matrix-container{overflow:auto}
.matrix-cell{width:14px;height:14px;border:1px solid var(--bg0);transition:all 0.15s}
.matrix-cell:hover{transform:scale(1.5);z-index:10}
.matrix-label{font-size:8px;color:var(--t2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.arc-container{width:100%;height:100%}
.arc-node{cursor:pointer;transition:all 0.2s}
.arc-node:hover{transform:scale(1.3)}
.arc-link{fill:none;stroke-opacity:0.3;transition:stroke-opacity 0.2s}
.arc-link:hover{stroke-opacity:0.8}
.bundle-container{width:100%;height:100%}
.bundle-node{cursor:pointer}
.bundle-link{fill:none;stroke-opacity:0.2}
.viz-tabs{display:flex;gap:4px;margin-bottom:12px;padding:4px;background:var(--bg2);border-radius:8px}
.viz-tab{padding:8px 12px;border:none;background:transparent;color:var(--t2);font-size:10px;font-weight:600;border-radius:6px;cursor:pointer;display:flex;align-items:center;gap:6px}
.viz-tab:hover{background:var(--bg3);color:var(--t1)}
.viz-tab.active{background:var(--acc);color:var(--bg0)}
.viz-selector{position:absolute;top:12px;left:50%;transform:translateX(-50%);display:flex;gap:4px;background:var(--bg1);border:1px solid var(--border);border-radius:8px;padding:4px;z-index:50}
.viz-selector-btn{padding:6px 12px;border:none;background:transparent;color:var(--t2);font-size:9px;font-weight:600;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px}
.viz-selector-btn:hover{background:var(--bg3);color:var(--t0)}
.viz-selector-btn.active{background:var(--accbg);color:var(--acc)}
.treemap-tooltip{position:absolute;padding:8px 12px;background:var(--bg1);border:1px solid var(--border);border-radius:6px;font-size:10px;pointer-events:none;z-index:100;max-width:200px}
.treemap-tooltip-title{font-weight:600;color:var(--t0);margin-bottom:4px}
.treemap-tooltip-stat{color:var(--t2);display:flex;justify-content:space-between;gap:12px}
.matrix-row{display:flex;align-items:center}
.matrix-label-row{writing-mode:vertical-lr;transform:rotate(180deg);font-size:8px;color:var(--t2);padding:2px 4px;max-width:80px;overflow:hidden;text-overflow:ellipsis}
.matrix-label-col{font-size:8px;color:var(--t2);padding:4px 4px;width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.heatmap-legend{position:absolute;bottom:60px;right:20px;background:var(--bg1);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:9px}
.heatmap-gradient{width:100px;height:8px;border-radius:4px;margin:6px 0;background:linear-gradient(90deg,var(--bg3),var(--acc))}
.pr-impact-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.pr-impact-card{background:var(--bg0);border:1px solid var(--border);border-radius:10px;padding:14px;overflow:hidden}
.pr-impact-card-title{font-size:10px;font-weight:600;color:var(--t3);text-transform:uppercase;margin-bottom:12px;display:flex;align-items:center;gap:6px}
.pr-risk-meter{display:flex;flex-direction:column;align-items:center;padding:20px}
.pr-risk-circle{width:100px;height:100px;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;border:4px solid}
.pr-risk-value{font-size:32px;font-weight:700}
.pr-risk-text{font-size:10px;font-weight:600;text-transform:uppercase}
.pr-metric-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border2)}
.pr-metric-row:last-child{border-bottom:none}
.pr-metric-label{font-size:10px;color:var(--t2)}
.pr-metric-value{font-size:10px;font-weight:600;color:var(--t0)}
.pr-dependency-chain{display:flex;flex-wrap:wrap;gap:4px;align-items:center}
.pr-chain-node{padding:4px 8px;background:var(--bg2);border-radius:4px;font-size:9px;color:var(--t1)}
.pr-chain-node.changed{background:var(--accbg);color:var(--acc);border:1px solid var(--acc)}
.pr-chain-arrow{color:var(--t3);font-size:10px}
.pr-reviewer-card{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;margin-bottom:6px;background:var(--bg2)}
.pr-reviewer-avatar{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:600;color:white}
.pr-reviewer-info{flex:1}
.pr-reviewer-name{font-size:11px;font-weight:600;color:var(--t0)}
.pr-reviewer-reason{font-size:9px;color:var(--t3)}
.pr-test-impact{display:flex;flex-direction:column;gap:6px}
.pr-test-file{display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--bg2);border-radius:4px;font-size:10px}
.pr-test-icon{font-size:12px}
.pr-hotspot{display:flex;align-items:center;gap:8px;padding:8px;background:var(--bg2);border-radius:6px;margin-bottom:6px}
.pr-hotspot-bar{flex:1;height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}
.pr-hotspot-fill{height:100%;border-radius:3px}
.conn-item{border-bottom:1px solid var(--border2)}
.conn-item:last-child{border-bottom:none}
.conn-header{display:flex;align-items:center;gap:6px;padding:8px 12px;cursor:pointer;transition:background 0.15s}
.conn-header:hover{background:var(--bg2)}
.conn-file-icon{font-size:12px}
.conn-file-name{font-size:10px;font-weight:500;color:var(--t1)}
.conn-fns{padding:6px 12px 10px 28px;background:var(--bg2);border-top:1px solid var(--border2)}
.conn-fn{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;margin:2px 0;background:var(--bg1);border-radius:4px;font-size:9px}
.conn-fn-name{color:var(--acc);font-family:'JetBrains Mono',monospace}
.conn-fn-count{color:var(--t3);font-size:8px}
.conn-goto{margin-top:8px;padding:6px 10px;background:var(--accbg);color:var(--acc);border-radius:4px;font-size:9px;font-weight:600;cursor:pointer;text-align:center;transition:all 0.15s}
.conn-goto:hover{background:var(--acc);color:var(--bg0)}
.file-preview-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:1100;backdrop-filter:blur(4px)}
.file-preview-modal{background:var(--bg0);border:1px solid var(--border);border-radius:12px;width:90vw;max-width:1000px;height:85vh;display:flex;flex-direction:column;box-shadow:0 25px 50px -12px rgba(0,0,0,0.5)}
.file-preview-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:var(--bg1);border-bottom:1px solid var(--border);border-radius:12px 12px 0 0}
.file-preview-title{display:flex;align-items:center;gap:10px}
.file-preview-icon{font-size:18px}
.file-preview-name{font-size:13px;font-weight:600;color:var(--t0)}
.file-preview-path{font-size:10px;color:var(--t3);margin-left:8px;font-family:'JetBrains Mono',monospace}
.file-preview-actions{display:flex;align-items:center;gap:8px}
.file-preview-line-badge{font-size:10px;padding:4px 10px;background:var(--accbg);color:var(--acc);border-radius:6px;font-weight:600}
.file-preview-close{background:transparent;border:1px solid var(--border);color:var(--t2);width:32px;height:32px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all 0.15s}
.file-preview-close:hover{background:var(--red);border-color:var(--red);color:white}
.file-preview-content{flex:1;overflow:auto;position:relative}
.file-preview-code{margin:0;padding:16px 0;font-family:'JetBrains Mono',monospace;font-size:12px;line-height:1.6;background:var(--bg0);counter-reset:line;min-height:100%}
.file-preview-line{display:flex;min-height:19px}
.file-preview-line:hover{background:var(--bg2)}
.file-preview-line.highlighted{background:rgba(0,255,157,0.15);border-left:3px solid var(--acc)}
.file-preview-line.highlighted .file-preview-linenum{color:var(--acc);font-weight:600}
.file-preview-linenum{display:inline-block;min-width:50px;padding:0 16px 0 16px;text-align:right;color:var(--t3);user-select:none;border-right:1px solid var(--border2);flex-shrink:0}
.file-preview-text{flex:1;padding:0 16px;white-space:pre;overflow-x:visible;color:var(--t1)}
.file-preview-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:16px}
.file-preview-loading .spinner{width:32px;height:32px;border-width:2px}
.file-preview-loading-text{font-size:11px;color:var(--t3)}
.file-preview-error{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:var(--t3)}
.file-preview-error-icon{font-size:48px;opacity:0.5}
.view-file-btn{font-size:9px;padding:4px 8px;background:var(--bg2);border:1px solid var(--border);color:var(--t2);border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;gap:4px;transition:all 0.15s}
.view-file-btn:hover{background:var(--accbg);border-color:var(--acc);color:var(--acc)}
.syn-kw{color:#c678dd}
.syn-str{color:#98c379}
.syn-num{color:#d19a66}
.syn-com{color:#5c6370;font-style:italic}
.syn-fn{color:#61afef}
.syn-type{color:#e5c07b}
.syn-op{color:#56b6c2}
.syn-var{color:#e06c75}
.syn-tag{color:#e06c75}
.syn-attr{color:#d19a66}
.syn-punct{color:#abb2bf}
.light .syn-kw{color:#a626a4}
.light .syn-str{color:#50a14f}
.light .syn-num{color:#986801}
.light .syn-com{color:#a0a1a7}
.light .syn-fn{color:#4078f2}
.light .syn-type{color:#c18401}
.light .syn-op{color:#0184bc}
.light .syn-var{color:#e45649}
.light .syn-tag{color:#e45649}
.light .syn-attr{color:#986801}
.light .syn-punct{color:#383a42}
.auth-select{padding:6px 10px;background:var(--bg0);border:1px solid var(--border);border-radius:6px;color:var(--t0);font-family:inherit;font-size:10px;cursor:pointer;min-width:100px;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b8b95' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:28px}
.auth-select:focus{outline:none;border-color:var(--acc)}
.auth-select:hover{border-color:var(--acc)}
.auth-select option{background:var(--bg1);color:var(--t0);padding:8px}
.auth-inputs{display:flex;gap:6px;align-items:center}
.auth-inputs .repo-input{transition:all 0.2s ease}
.private-key-btn{padding:6px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--t1);font-family:inherit;font-size:10px;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:4px}
.private-key-btn:hover{background:var(--bg4);border-color:var(--acc);color:var(--acc)}
.private-key-btn.has-key{background:var(--accbg);border-color:var(--acc);color:var(--acc)}
.key-modal{max-width:550px}
.key-modal .form-input{min-height:200px;font-family:'JetBrains Mono',monospace;font-size:10px;resize:vertical}
.key-info{font-size:10px;color:var(--t2);margin-bottom:12px;padding:10px;background:var(--bg0);border-radius:6px;line-height:1.5}
.key-info code{background:var(--bg3);padding:2px 5px;border-radius:3px;color:var(--acc)}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const{useState,useEffect,useRef,useMemo,useCallback}=React;
const COLORS=['#4d9fff','#a78bfa','#22d3ee','#00ff9d','#ff9f43','#ec4899','#ff5f5f','#84cc16'];
const LAYER_COLORS={ui:'#4d9fff',components:'#22d3ee',services:'#a78bfa',utils:'#00ff9d',data:'#ff9f43',config:'#ec4899',test:'#f59e0b',modules:'#a78bfa',forms:'#22d3ee',classes:'#ff9f43'};
const IGNORE=new Set(['node_modules','.git','vendor','dist','build','__pycache__','.next','coverage','.venv','venv','env','.env','.tox','.mypy_cache','.pytest_cache','.ruff_cache','__pypackages__','.eggs']);
const Parser={
// tree-sitter Python parser (real AST parser, loaded async via WASM)
_tsParser:null,
_tsInitPromise:null,
initTreeSitter:async function(){
if(this._tsParser)return this._tsParser;
if(this._tsInitPromise)return this._tsInitPromise;
this._tsInitPromise=(async()=>{
if(typeof TreeSitter==='undefined')return null;
try{
await TreeSitter.init({
locateFile:function(scriptName){
return 'https://cdn.jsdelivr.net/npm/web-tree-sitter@0.25.10/'+scriptName;
}
});
var parser=new TreeSitter();
var Python=await TreeSitter.Language.load(
'https://cdn.jsdelivr.net/npm/tree-sitter-wasms@0.1.13/out/tree-sitter-python.wasm'
);
parser.setLanguage(Python);
this._tsParser=parser;
return parser;
}catch(e){
return null;
}
})();
return this._tsInitPromise;
},
codeExts:['.js','.jsx','.ts','.tsx','.mjs','.cjs','.py','.pyw','.pyi','.java','.go','.rb','.php','.vue','.svelte','.rs','.c','.cpp','.cc','.h','.hpp','.cs','.swift','.kt','.kts','.scala','.clj','.ex','.exs','.erl','.hs','.lua','.r','.R','.jl','.dart','.elm','.fs','.fsx','.ml','.pl','.pm','.sh','.bash','.zsh','.fish','.ps1','.psm1','.groovy','.gradle','.vba','.bas','.cls','.xlsm','.xlam','.xlsb','.xla','.xlw'],
textExts:['.md','.txt','.json','.yaml','.yml','.toml','.xml','.html','.htm','.css','.scss','.sass','.less','.svg','.graphql','.gql','.sql','.prisma','.proto','.tf','.tfvars','.dockerfile','.env','.env.example','.gitignore','.eslintrc','.prettierrc','.babelrc','.editorconfig','.ini','.cfg','.conf','.properties','.lock','.csv','.rst','.tex','.makefile','.cmake','.rake','.vba','.bas','.cls','.xlsm','.xlam','.xlsb','.xla','.xlw'],
binExts:['.png','.jpg','.jpeg','.gif','.ico','.webp','.bmp','.svg','.woff','.woff2','.ttf','.eot','.otf','.pdf','.zip','.tar','.gz','.rar','.7z','.exe','.dll','.so','.dylib','.bin','.dat','.db','.sqlite','.mp3','.mp4','.wav','.avi','.mov','.webm'],
isCode:function(n){return Parser.codeExts.some(function(e){return n.toLowerCase().endsWith(e);});},
isText:function(n){return Parser.textExts.some(function(e){return n.toLowerCase().endsWith(e);});},
isBinary:function(n){return Parser.binExts.some(function(e){return n.toLowerCase().endsWith(e);});},
isIncluded:function(n){return !Parser.isBinary(n);},
isVBA:function(n){return ['.vba','.bas','.cls','.xlsm','.xlam','.xlsb','.xla','.xlw'].some(function(e){return n.toLowerCase().endsWith(e);});},
isHTML:function(n){return ['.html','.htm','.xhtml'].some(function(e){return n.toLowerCase().endsWith(e);});},
isCSS:function(n){return ['.css','.scss','.sass','.less'].some(function(e){return n.toLowerCase().endsWith(e);});},
isJSON:function(n){return ['.json'].some(function(e){return n.toLowerCase().endsWith(e);});},
detectLayer:function(p){
var l=p.toLowerCase();
// Test files
if(l.includes('/test')||l.match(/test_\w+\.py$/)||l.match(/\w+_test\.py$/)||l.includes('conftest'))return'test';
// UI/View layer
if(l.includes('/ui/')||l.includes('/views/')||l.includes('/pages/')||l.includes('/templates/')||l.includes('/static/'))return'ui';
if(l.includes('/component'))return'components';
// Service/API layer
if(l.includes('/service')||l.includes('/api/')||l.includes('/controller')||l.includes('/endpoint')||l.includes('/router'))return'services';
// Python middleware/handler layer
if(l.includes('/middleware')||l.includes('/handler')||l.includes('/signal'))return'services';
// Utility/Helper layer
if(l.includes('/util')||l.includes('/helper')||l.includes('/lib/')||l.includes('/common/'))return'utils';
// Data/Model layer
if(l.includes('/data')||l.includes('/model')||l.includes('/store')||l.includes('/schema')||l.includes('/serializer'))return'data';
// Python-specific data layers
if(l.includes('/migration'))return'data';
if(l.includes('/fixtures/'))return'data';
// Task/Worker layer
if(l.includes('/task')||l.includes('/worker')||l.includes('/celery')||l.includes('/job'))return'services';
// Config layer
if(l.includes('/config')||l.includes('/settings')||l.match(/settings\.py$/))return'config';
// VBA-specific layer detection
if(l.includes('/modules/')||l.includes('/bas/'))return'modules';
if(l.includes('/forms/')||l.includes('/userforms/'))return'ui';
if(l.includes('/classes/'))return'data';
if(l.includes('/standard/'))return'utils';
return'utils';
},
detectPatterns:function(files){
var patterns=[];
var singletons=files.filter(function(f){return f.content&&(f.content.includes('getInstance')||f.content.match(/let\s+instance\s*=/)||f.content.match(/private\s+static\s+instance/));});
if(singletons.length)patterns.push({name:'Singleton',icon:'🔒',desc:'Ensures a class has only one instance. Common for configuration, logging, or connection pools.',severity:'info',files:singletons.map(function(f){return{name:f.name,path:f.path};}),metrics:{instances:singletons.length}});
var factories=files.filter(function(f){return f.content&&(f.name.toLowerCase().includes('factory')||f.content.match(/create[A-Z]\w*\s*\(/)||f.content.includes('return new'));});
if(factories.length)patterns.push({name:'Factory',icon:'🏭',desc:'Creates objects without specifying exact class. Enables loose coupling and extensibility.',severity:'info',files:factories.map(function(f){return{name:f.name,path:f.path};}),metrics:{factories:factories.length}});
var observers=files.filter(function(f){return f.content&&(f.content.includes('subscribe')||f.content.includes('addEventListener')||f.content.includes('.on(')||f.content.includes('emit('));});
if(observers.length)patterns.push({name:'Observer/Event',icon:'👁️',desc:'Defines a subscription mechanism for event-driven architecture. Great for decoupling.',severity:'info',files:observers.map(function(f){return{name:f.name,path:f.path};}),metrics:{emitters:observers.length}});
var hooks=files.filter(function(f){return f.content&&f.content.match(/export\s+(?:const|function)\s+use[A-Z]/);});
if(hooks.length)patterns.push({name:'Custom Hooks',icon:'🪝',desc:'React hooks for reusable stateful logic. Promotes code reuse and separation of concerns.',severity:'info',files:hooks.map(function(f){return{name:f.name,path:f.path};}),metrics:{hooks:hooks.length}});
var hocs=files.filter(function(f){return f.content&&(f.content.match(/with[A-Z]\w*\s*=\s*\(/)||f.content.match(/export\s+default\s+connect/));});
if(hocs.length)patterns.push({name:'Higher-Order Component',icon:'🎁',desc:'Functions that take a component and return an enhanced component.',severity:'info',files:hocs.map(function(f){return{name:f.name,path:f.path};}),metrics:{hocs:hocs.length}});
var providers=files.filter(function(f){return f.content&&(f.content.includes('createContext')||f.content.includes('Provider')||f.content.includes('useContext'));});
if(providers.length)patterns.push({name:'Context Provider',icon:'🌐',desc:'React Context for global state. Alternative to prop drilling.',severity:'info',files:providers.map(function(f){return{name:f.name,path:f.path};}),metrics:{contexts:providers.length}});
// VBA-specific patterns
var vbaUserForms=files.filter(function(f){return f.content&&(f.content.match(/Attribute\s+VB_Name\s*=\s*["']UserForm/i)||f.name.match(/UserForm/i));});
if(vbaUserForms.length)patterns.push({name:'UserForms',icon:'🖼️',desc:'VBA UserForms for UI components. Common in Excel/Access automation.',severity:'info',files:vbaUserForms.map(function(f){return{name:f.name,path:f.path};}),metrics:{forms:vbaUserForms.length}});
var vbaModules=files.filter(function(f){return f.content&&(f.content.match(/Attribute\s+VB_Name\s*=\s*["']Module/i)||f.name.match(/Module/i));});
if(vbaModules.length)patterns.push({name:'Modules',icon:'📦',desc:'VBA Modules for reusable code and business logic.',severity:'info',files:vbaModules.map(function(f){return{name:f.name,path:f.path};}),metrics:{modules:vbaModules.length}});
var vbaClasses=files.filter(function(f){return f.content&&(f.content.match(/Attribute\s+VB_Name\s*=\s*["']Class/i)||f.name.match(/Class/i));});
if(vbaClasses.length)patterns.push({name:'Class Modules',icon:'🏛️',desc:'VBA Class Modules for object-oriented programming patterns.',severity:'info',files:vbaClasses.map(function(f){return{name:f.name,path:f.path};}),metrics:{classes:vbaClasses.length}});
// Python-specific patterns
var decoratorFiles=files.filter(function(f){return f.content&&f.name.endsWith('.py')&&f.content.match(/@\w+\s*(?:\(.*\))?\s*\n\s*(?:def|class)/);});
var pyDecorators=decoratorFiles.filter(function(f){return f.content.match(/@(?:app\.route|router\.|blueprint\.|get|post|put|delete|patch)\s*\(/);});
if(pyDecorators.length)patterns.push({name:'Route Decorators',icon:'🛤️',desc:'Flask/FastAPI/Django route decorators for URL routing. Common in Python web frameworks.',severity:'info',files:pyDecorators.map(function(f){return{name:f.name,path:f.path};}),metrics:{routes:pyDecorators.length}});
var dataclasses=files.filter(function(f){return f.content&&f.name.endsWith('.py')&&f.content.match(/@dataclass/);});
if(dataclasses.length)patterns.push({name:'Dataclasses',icon:'📋',desc:'Python dataclasses for structured data. Reduces boilerplate for data-holding classes.',severity:'info',files:dataclasses.map(function(f){return{name:f.name,path:f.path};}),metrics:{dataclasses:dataclasses.length}});
var abcFiles=files.filter(function(f){return f.content&&f.name.endsWith('.py')&&(f.content.match(/\bABC\b/)||f.content.match(/@abstractmethod/)||f.content.match(/ABCMeta/));});
if(abcFiles.length)patterns.push({name:'Abstract Base Classes',icon:'🏗️',desc:'Python ABCs enforce interface contracts. Ensures subclasses implement required methods.',severity:'info',files:abcFiles.map(function(f){return{name:f.name,path:f.path};}),metrics:{abcs:abcFiles.length}});
var ctxManagers=files.filter(function(f){return f.content&&f.name.endsWith('.py')&&(f.content.match(/@contextmanager/)||f.content.match(/def\s+__enter__/));});
if(ctxManagers.length)patterns.push({name:'Context Managers',icon:'🔄',desc:'Python context managers for resource management (with statement). Ensures proper cleanup.',severity:'info',files:ctxManagers.map(function(f){return{name:f.name,path:f.path};}),metrics:{managers:ctxManagers.length}});
var pyMixins=files.filter(function(f){return f.content&&f.name.endsWith('.py')&&f.content.match(/class\s+\w*Mixin\w*\s*[\(:]?/);});
if(pyMixins.length)patterns.push({name:'Mixins',icon:'🧩',desc:'Python mixins for reusable behavior through multiple inheritance.',severity:'info',files:pyMixins.map(function(f){return{name:f.name,path:f.path};}),metrics:{mixins:pyMixins.length}});
var pySignals=files.filter(function(f){return f.content&&f.name.endsWith('.py')&&(f.content.match(/Signal\s*\(/)||f.content.match(/@receiver\s*\(/)||f.content.match(/\.connect\s*\(/));});
if(pySignals.length)patterns.push({name:'Django Signals',icon:'📡',desc:'Django signals for decoupled event-driven communication between components.',severity:'info',files:pySignals.map(function(f){return{name:f.name,path:f.path};}),metrics:{signals:pySignals.length}});
var pyMiddleware=files.filter(function(f){return f.content&&f.name.endsWith('.py')&&(f.content.match(/class\s+\w*Middleware/)||f.content.match(/def\s+middleware\s*\(/)||f.name.toLowerCase().includes('middleware'));});
if(pyMiddleware.length)patterns.push({name:'Middleware',icon:'🔗',desc:'Request/response middleware for cross-cutting concerns (auth, logging, CORS).',severity:'info',files:pyMiddleware.map(function(f){return{name:f.name,path:f.path};}),metrics:{middleware:pyMiddleware.length}});
var godFiles=files.filter(function(f){return f.isCode!==false&&f.functions&&f.functions.length>15;});
if(godFiles.length)patterns.push({name:'God Object',icon:'⚠️',desc:'Files with too many responsibilities (15+ functions). Consider splitting into smaller modules.',severity:'warning',isAnti:true,files:godFiles.map(function(f){return{name:f.name,path:f.path,fns:f.functions.length};}),metrics:{files:godFiles.length,avgFns:Math.round(godFiles.reduce(function(s,f){return s+f.functions.length;},0)/godFiles.length)}});
var longFiles=files.filter(function(f){return f.isCode!==false&&f.lines&&f.lines>500;});
if(longFiles.length)patterns.push({name:'Long File',icon:'📜',desc:'Files over 500 lines are harder to maintain. Consider breaking into smaller modules.',severity:'warning',isAnti:true,files:longFiles.map(function(f){return{name:f.name,path:f.path,lines:f.lines};}),metrics:{files:longFiles.length,avgLines:Math.round(longFiles.reduce(function(s,f){return s+f.lines;},0)/longFiles.length)}});
// VBA-specific anti-patterns
var vbaGodFiles=files.filter(function(f){return f.isCode!==false&&f.functions&&f.functions.length>20;});
if(vbaGodFiles.length)patterns.push({name:'VBA God Module',icon:'⚠️',desc:'VBA modules with 20+ procedures. Consider splitting into smaller modules.',severity:'warning',isAnti:true,files:vbaGodFiles.map(function(f){return{name:f.name,path:f.path,fns:f.functions.length,lines:f.lines};}),metrics:{files:vbaGodFiles.length,avgFns:Math.round(vbaGodFiles.reduce(function(s,f){return s+f.functions.length;},0)/vbaGodFiles.length)}});
return patterns;
},
detectDuplicates:function(files,allFns){
var duplicates=[];
// Common function names that are expected to be duplicated across files
// These are idiomatic patterns, not DRY violations
var commonNames=new Set([
// React lifecycle and handlers
'render','componentDidMount','componentWillUnmount','componentDidUpdate',
'shouldComponentUpdate','getDerivedStateFromProps','getSnapshotBeforeUpdate',
'handleClick','handleChange','handleSubmit','handleInput','handleKeyDown',
'handleKeyUp','handleKeyPress','handleBlur','handleFocus','handleScroll',
'handleMouseEnter','handleMouseLeave','handleDrag','handleDrop',
'onClick','onChange','onSubmit','onBlur','onFocus','onKeyDown',
// Common utility names
'init','setup','cleanup','destroy','reset','clear','update','refresh',
'validate','parse','format','transform','convert','process','execute',
'get','set','fetch','load','save','create','delete','remove','add',
'find','filter','map','reduce','sort','merge','clone','copy',
// Test patterns
'beforeEach','afterEach','beforeAll','afterAll','describe','it','test',
'setUp','tearDown','mock',
// Common class methods
'toString','valueOf','equals','hashCode','compare','clone',
'serialize','deserialize','toJSON','fromJSON',
// Express/API patterns
'index','show','store','update','destroy','create','edit',
// Python common patterns
'__init__','__str__','__repr__','__len__','__eq__','__hash__','__enter__','__exit__',
'__getattr__','__setattr__','__delattr__','__getitem__','__setitem__','__contains__',
'__iter__','__next__','__call__','__bool__','__lt__','__gt__','__le__','__ge__',
'upgrade','downgrade','setUp','tearDown','setUpClass','tearDownClass',
'main','create_app','configure','register','on_startup','on_shutdown','lifespan',
// Vue lifecycle
'mounted','created','updated','destroyed','beforeCreate','beforeMount',
// Angular lifecycle
'ngOnInit','ngOnDestroy','ngOnChanges','ngAfterViewInit',
// Svelte
'onMount','onDestroy'
]);
// Group functions by name (excluding common names)
var fnByName={};
allFns.forEach(function(fn){
// Skip common/idiomatic names
if(commonNames.has(fn.name))return;
// Skip very short names (likely false positives)
if(fn.name.length<3)return;
// Skip class methods (same method name in different classes is normal)
if(fn.isClassMethod)return;
// Skip Python class-scoped names (ClassName.method)
if(fn.name.includes('.'))return;
// Skip decorated functions (framework handlers have similar structures by design)
if(fn.decorators&&fn.decorators.length>0)return;
if(!fnByName[fn.name])fnByName[fn.name]=[];
fnByName[fn.name].push(fn);
});
// Find duplicate names across different files - only report if suspicious
Object.entries(fnByName).forEach(function(entry){
var name=entry[0],fns=entry[1];
var uniqueFiles=[...new Set(fns.map(function(f){return f.file;}))];
// Only flag if in 3+ files (2 files might be intentional)
if(uniqueFiles.length>=3){
// Check if the code is actually similar (not just same name)
var codeSamples=fns.filter(function(f){return f.code&&f.code.length>30;});
if(codeSamples.length>=2){
// Compare first two code samples for similarity
var sim=Parser.codeSimilarity(codeSamples[0].code,codeSamples[1].code);
if(sim>0.5){ // More than 50% similar - likely a real duplicate
duplicates.push({
type:'name',
name:name,
count:uniqueFiles.length,
files:fns.map(function(f){return{file:f.file,line:f.line};}),
similarity:Math.round(sim*100),
suggestion:'Function "'+name+'" appears in '+uniqueFiles.length+' files with '+Math.round(sim*100)+'% similarity - consider consolidating'
});
}
}
}
});
// Find similar code blocks (improved algorithm)
// Use structural hash that captures the essence of the code
var codeGroups={};
allFns.forEach(function(fn){
if(!fn.code||fn.code.length<80)return; // Skip very short functions
// Create a structural fingerprint
var fingerprint=Parser.codeFingerprint(fn.code);
if(!fingerprint)return;
if(!codeGroups[fingerprint])codeGroups[fingerprint]=[];
codeGroups[fingerprint].push(fn);
});
Object.values(codeGroups).forEach(function(fns){
if(fns.length>1){
var uniqueFiles=[...new Set(fns.map(function(f){return f.file;}))];
// Must be in different files to be a real duplication issue
if(uniqueFiles.length>1){
// Verify with actual similarity check
var sim=Parser.codeSimilarity(fns[0].code,fns[1].code);
if(sim>0.7){ // 70% or more similar
duplicates.push({
type:'code',
name:fns.map(function(f){return f.name;}).join(', '),
count:fns.length,
files:fns.map(function(f){return{file:f.file,name:f.name,line:f.line};}),
similarity:Math.round(sim*100),
suggestion:'Similar code blocks ('+Math.round(sim*100)+'% match) - consider extracting to a shared utility'
});
}
}
}
});
return duplicates;
},
// Calculate code similarity using normalized comparison (0-1 scale)
codeSimilarity:function(code1,code2){
if(!code1||!code2)return 0;
// Normalize both code blocks
function normalize(code){
return code
.replace(/\/\/.*$/gm,'') // Remove JS single-line comments
.replace(/#.*$/gm,'') // Remove Python/Ruby comments
.replace(/\/\*[\s\S]*?\*\//g,'') // Remove multi-line comments
.replace(/"""[\s\S]*?"""/g,'S') // Remove Python docstrings (triple double)
.replace(/'''[\s\S]*?'''/g,'S') // Remove Python docstrings (triple single)
.replace(/['"`][^'"`]*['"`]/g,'S') // Normalize strings
.replace(/\b\d+\.?\d*\b/g,'N') // Normalize numbers
.replace(/\s+/g,' ') // Normalize whitespace
.trim();
}
var n1=normalize(code1);
var n2=normalize(code2);
if(n1===n2)return 1;
if(n1.length===0||n2.length===0)return 0;
// Use longest common subsequence ratio
var lcs=Parser.lcsLength(n1,n2);
var maxLen=Math.max(n1.length,n2.length);
return lcs/maxLen;
},
// Longest common subsequence length (optimized for similarity)
lcsLength:function(s1,s2){
// Use simplified approach for performance
if(s1.length>500||s2.length>500){
// For long strings, use sampling
s1=s1.substring(0,500);
s2=s2.substring(0,500);
}
var m=s1.length,n=s2.length;
var prev=new Array(n+1).fill(0);
var curr=new Array(n+1).fill(0);
for(var i=1;i<=m;i++){
for(var j=1;j<=n;j++){
if(s1[i-1]===s2[j-1]){
curr[j]=prev[j-1]+1;
}else{
curr[j]=Math.max(prev[j],curr[j-1]);
}
}
var tmp=prev;prev=curr;curr=tmp;
curr.fill(0);
}
return prev[n];
},
// Create a structural fingerprint for code (for grouping similar code)
codeFingerprint:function(code){
if(!code||code.length<50)return null;
// Extract structural elements
var structure=code
.replace(/\/\/.*$/gm,'') // Remove comments
.replace(/\/\*[\s\S]*?\*\//g,'')
.replace(/['"`][^'"`]*['"`]/g,'') // Remove string contents
.replace(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g,'I') // All identifiers -> I
.replace(/\b\d+\.?\d*\b/g,'N') // All numbers -> N
.replace(/\s+/g,''); // Remove whitespace
// Take a hash-like fingerprint based on structure length and key patterns
var patterns={
loops:(structure.match(/for|while/g)||[]).length,
conditions:(structure.match(/if|\?/g)||[]).length,
calls:(structure.match(/I\(/g)||[]).length,
returns:(structure.match(/return/g)||[]).length,
len:Math.floor(structure.length/50)*50 // Bucket by length
};
// Create fingerprint string
return 'L'+patterns.loops+'C'+patterns.conditions+'F'+patterns.calls+'R'+patterns.returns+'S'+patterns.len;
},
detectLayerViolations:function(files,connections){
var violations=[];
var layerOrder={presentation:0,ui:0,component:0,components:0,page:0,view:0,feature:1,service:2,services:2,api:2,data:3,model:3,util:4,utils:4,helper:4,lib:4,core:4,config:5,test:6,modules:5,forms:0,classes:3};
connections.forEach(function(c){
var srcFile=files.find(function(f){return f.path===c.source;});
var tgtFile=files.find(function(f){return f.path===c.target;});
if(!srcFile||!tgtFile)return;
var srcLayer=(srcFile.layer||'').toLowerCase();
var tgtLayer=(tgtFile.layer||'').toLowerCase();
var srcLevel=layerOrder[srcLayer];
var tgtLevel=layerOrder[tgtLayer];
// Violation: lower layer importing from higher layer (e.g., service importing from UI)
if(srcLevel!==undefined&&tgtLevel!==undefined&&srcLevel>tgtLevel&&srcLevel-tgtLevel>1){
violations.push({
from:srcFile.path,
fromLayer:srcFile.layer,
to:tgtFile.path,
toLayer:tgtFile.layer,
fn:c.fn,
suggestion:srcFile.layer+' should not import from '+tgtFile.layer+'. Consider inverting the dependency or using dependency injection.'
});
}
});
return violations;
},
calcComplexity:function(content){
if(!content)return{score:0,level:'low'};
// Approximate cyclomatic complexity - supports JS, Python, and other languages
var complexity=1;
// JS/C-style patterns
var patterns=[/\bif\s*\(/g,/\belse\s+if\s*\(/g,/\bwhile\s*\(/g,/\bfor\s*\(/g,/\bcase\s+/g,/\bcatch\s*\(/g,/\?\s*[^:]+\s*:/g,/&&/g,/\|\|/g];
// Python-specific patterns
var pyPatterns=[/\bif\s+[^(]/g,/\belif\s+/g,/\bwhile\s+[^(]/g,/\bfor\s+\w+\s+in\s+/g,/\bexcept\s*/g,/\bwith\s+/g,/\band\b/g,/\bor\b/g,/\bif\s+.+\s+else\s+/g,/\bfor\s+.+\s+in\s+[^\n]*\]/g];
patterns.concat(pyPatterns).forEach(function(p){var m=content.match(p);if(m)complexity+=m.length;});
// Deduplicate: if both `if (` and `if ` match the same lines, the count is inflated
// but for a quick approximation this is acceptable
var level='low';
if(complexity>30)level='critical';
else if(complexity>20)level='high';
else if(complexity>10)level='medium';
return{score:complexity,level:level};
},
generateSuggestions:function(data){
var suggestions=[];
// Based on dead functions
if(data.stats.dead>10){
suggestions.push({priority:'high',icon:'🧹',title:'Remove Dead Code',desc:data.stats.dead+' unused functions detected. Removing them will improve maintainability and reduce bundle size.',action:'Review unused functions in the Issues panel',impact:'Reduces codebase by ~'+(data.stats.dead*15)+' lines'});
}
// Based on circular dependencies
var circular=data.issues.filter(function(i){return i.title&&i.title.includes('Circular');});
if(circular.length){
suggestions.push({priority:'critical',icon:'🔄',title:'Break Circular Dependencies',desc:circular.length+' circular dependencies found. These cause tight coupling and make testing difficult.',action:'Extract shared code to a new module or use dependency injection',impact:'Improves testability and modularity'});
}
// Based on god files
var godFiles=data.issues.filter(function(i){return i.title&&i.title.includes('Large');});
if(godFiles.length){
suggestions.push({priority:'high',icon:'✂️',title:'Split Large Files',desc:godFiles.length+' files have too many functions. Split by responsibility.',action:'Group related functions and extract to separate modules',impact:'Improves code navigation and testing'});
}
// Based on high coupling
var coupling=data.issues.filter(function(i){return i.title&&i.title.includes('Coupled');});
if(coupling.length){
suggestions.push({priority:'medium',icon:'🔗',title:'Reduce Coupling',desc:coupling.length+' files are imported by many others. Consider if this is intentional.',action:'Review if these should be split or if importers should be consolidated',impact:'Reduces blast radius of changes'});
}
// Based on duplicates
if(data.duplicates&&data.duplicates.length>0){
var nameDups=data.duplicates.filter(function(d){return d.type==='name';});
var codeDups=data.duplicates.filter(function(d){return d.type==='code';});
if(nameDups.length){
suggestions.push({priority:'medium',icon:'📛',title:'Resolve Naming Conflicts',desc:nameDups.length+' function names are duplicated across files. This can cause confusion.',action:'Rename functions to be more specific or consolidate into shared module',impact:'Prevents bugs from importing wrong function'});
}
if(codeDups.length){
suggestions.push({priority:'high',icon:'📋',title:'Extract Duplicated Code',desc:codeDups.length+' instances of similar code found. DRY principle violation.',action:'Create shared utility functions',impact:'Reduces maintenance burden and potential bugs'});
}
}
// Based on layer violations
if(data.layerViolations&&data.layerViolations.length>0){
suggestions.push({priority:'high',icon:'🏗️',title:'Fix Architecture Violations',desc:data.layerViolations.length+' layer violations found. Lower layers should not depend on higher layers.',action:'Invert dependencies or use interfaces/events',impact:'Improves architecture and testability'});
}
// Based on security
var highSec=data.securityIssues?data.securityIssues.filter(function(s){return s.severity==='high';}):[];
if(highSec.length){
suggestions.push({priority:'critical',icon:'🔐',title:'Fix Security Issues',desc:highSec.length+' high-severity security issues found.',action:'Address hardcoded secrets, injection risks immediately',impact:'Prevents potential security breaches'});
}
// Test coverage hint
var testFiles=data.files.filter(function(f){return f.name.includes('.test.')||f.name.includes('.spec.')||f.path.includes('__tests__');});
var testRatio=data.files.length>0?(testFiles.length/data.files.length*100):0;
if(testRatio<10&&data.files.length>10){
suggestions.push({priority:'medium',icon:'🧪',title:'Add Test Coverage',desc:'Only '+testFiles.length+' test files found ('+Math.round(testRatio)+'%). Consider adding more tests.',action:'Focus on testing critical paths and high-complexity files',impact:'Prevents regressions and improves confidence'});
}
return suggestions.sort(function(a,b){var p={critical:0,high:1,medium:2,low:3};return p[a.priority]-p[b.priority];});
},
detectSecurity:function(files){
var issues=[];
files.forEach(function(f){
if(!f.content)return;
var lines=f.content.split('\n');
lines.forEach(function(line,idx){
if(line.match(/(?:password|passwd|pwd|secret|api_key|apikey|token|auth)\s*[=:]\s*['"][^'"]{4,}['"]/i)&&!line.includes('process.env')&&!line.includes('config.')){
issues.push({severity:'high',title:'Hardcoded Secret',file:f.name,path:f.path,line:idx+1,desc:'Credentials should never be hardcoded. Use environment variables or a secrets manager.',code:line.trim().substring(0,80)});
}
});
if(f.content.match(/query\s*\(\s*['"`][^'"`]*\s*\+/)||f.content.match(/execute\s*\(\s*['"`][^'"`]*\$\{/)||f.content.match(/\$\{.*\}.*(?:SELECT|INSERT|UPDATE|DELETE)/i)){
var m=f.content.match(/.*(query|execute|SELECT|INSERT|UPDATE|DELETE).*(\+|\$\{).*/i);
issues.push({severity:'high',title:'SQL Injection Risk',file:f.name,path:f.path,desc:'String concatenation in SQL queries. Use parameterized queries instead.',code:m?m[0].trim().substring(0,80):''});
}
if(f.content.match(/innerHTML\s*=/)||f.content.match(/dangerouslySetInnerHTML/)){
issues.push({severity:'high',title:'XSS Vulnerability',file:f.name,path:f.path,desc:'Direct HTML injection can lead to XSS attacks. Sanitize user input.',code:''});
}
if(f.content.includes('eval(')){
var evalLine=lines.findIndex(function(l){return l.includes('eval(');});
issues.push({severity:'medium',title:'Dynamic Code Execution',file:f.name,path:f.path,line:evalLine+1,desc:'eval() executes arbitrary code. Avoid if possible or validate input strictly.',code:evalLine>=0?lines[evalLine].trim().substring(0,80):''});
}
if(f.content.includes('Function(')||f.content.match(/new\s+Function\s*\(/)){
issues.push({severity:'medium',title:'Function Constructor',file:f.name,path:f.path,desc:'Function constructor is similar to eval(). Consider alternatives.',code:''});
}
if(f.content.match(/\.exec\s*\(/)||f.content.match(/child_process/)){
issues.push({severity:'medium',title:'Command Execution',file:f.name,path:f.path,desc:'Shell command execution detected. Ensure input is sanitized to prevent injection.',code:''});
}
if(f.content.match(/console\.(log|debug|info)\(/)){
var consoleCount=(f.content.match(/console\.(log|debug|info)\(/g)||[]).length;
if(consoleCount>3){
issues.push({severity:'low',title:'Debug Statements',file:f.name,path:f.path,desc:consoleCount+' console statements found. Remove before production.',code:''});
}
}
// VBA-specific security checks
if(f.content.match(/SendKeys\s*\(/i)){
issues.push({severity:'high',title:'SendKeys Usage',file:f.name,path:f.path,desc:'SendKeys can be exploited for code injection. Avoid using SendKeys.',code:''});
}
if(f.content.match(/Shell\s*\(/i)){
issues.push({severity:'high',title:'Shell Command Execution',file:f.name,path:f.path,desc:'Shell() executes system commands. Ensure input is validated.',code:''});
}
if(f.content.match(/CreateObject\s*\(\s*["']WScript\.Shell["']/i)){
issues.push({severity:'high',title:'WScript.Shell Creation',file:f.name,path:f.path,desc:'Creating WScript.Shell object allows command execution. Use with caution.',code:''});
}
if(f.content.match(/Application\.Run\s*\(/i)){
issues.push({severity:'medium',title:'Dynamic Code Execution',file:f.name,path:f.path,desc:'Application.Run can execute arbitrary code. Validate input.',code:''});
}
if(f.content.match(/On Error Resume Next/i)){
var errorResumeCount=(f.content.match(/On Error Resume Next/gi)||[]).length;
if(errorResumeCount>2){
issues.push({severity:'medium',title:'Excessive Error Suppression',file:f.name,path:f.path,desc:errorResumeCount+' instances of "On Error Resume Next" found. This can hide bugs.',code:''});
}
}
if(f.content.match(/TODO|FIXME|HACK|XXX/)){
var todoCount=(f.content.match(/TODO|FIXME|HACK|XXX/g)||[]).length;
issues.push({severity:'low',title:'Code Comments',file:f.name,path:f.path,desc:todoCount+' TODO/FIXME comments found. Address before release.',code:''});
}
// Python-specific security checks
var isPyFile=f.name.endsWith('.py')||f.name.endsWith('.pyw');
if(isPyFile&&f.content){
// eval() and exec() - arbitrary code execution
if(f.content.match(/\beval\s*\(/)){
var evalLine=lines.findIndex(function(l){return l.match(/\beval\s*\(/);});
issues.push({severity:'high',title:'Python eval()',file:f.name,path:f.path,line:evalLine>=0?evalLine+1:undefined,desc:'eval() executes arbitrary Python code. Use ast.literal_eval() for safe parsing.',code:evalLine>=0?lines[evalLine].trim().substring(0,80):''});
}
if(f.content.match(/\bexec\s*\(/)){
var execLine=lines.findIndex(function(l){return l.match(/\bexec\s*\(/);});
issues.push({severity:'high',title:'Python exec()',file:f.name,path:f.path,line:execLine>=0?execLine+1:undefined,desc:'exec() executes arbitrary Python code. This is almost always a security risk.',code:execLine>=0?lines[execLine].trim().substring(0,80):''});
}
// pickle - deserialization attacks
if(f.content.match(/\bpickle\.load/)||f.content.match(/\bunpickle/)){
issues.push({severity:'high',title:'Pickle Deserialization',file:f.name,path:f.path,desc:'pickle.load() can execute arbitrary code from untrusted data. Use JSON or safe alternatives.',code:''});
}
// subprocess with shell=True
if(f.content.match(/subprocess\.\w+\([^)]*shell\s*=\s*True/)){
issues.push({severity:'high',title:'Shell Injection Risk',file:f.name,path:f.path,desc:'subprocess with shell=True is vulnerable to command injection. Use shell=False with a list of args.',code:''});
}
// os.system / os.popen - command injection
if(f.content.match(/\bos\.system\s*\(/)||f.content.match(/\bos\.popen\s*\(/)){
var osLine=lines.findIndex(function(l){return l.match(/\bos\.(system|popen)\s*\(/);});
issues.push({severity:'high',title:'OS Command Execution',file:f.name,path:f.path,line:osLine>=0?osLine+1:undefined,desc:'os.system()/os.popen() are vulnerable to command injection. Use subprocess with shell=False.',code:osLine>=0?lines[osLine].trim().substring(0,80):''});
}
// __import__ - dynamic imports
if(f.content.match(/__import__\s*\(/)){
issues.push({severity:'medium',title:'Dynamic Import',file:f.name,path:f.path,desc:'__import__() with user input can load arbitrary modules. Validate module names against an allowlist.',code:''});
}
// Bare except clauses
var bareExcepts=(f.content.match(/\bexcept\s*:/g)||[]).length;
if(bareExcepts>2){
issues.push({severity:'medium',title:'Bare Except Clauses',file:f.name,path:f.path,desc:bareExcepts+' bare except: clauses found. These catch all exceptions including SystemExit and KeyboardInterrupt.',code:''});
}
// assert in non-test files
if(!f.name.includes('test')&&!f.path.includes('test')){
var assertCount=(f.content.match(/\bassert\s+/g)||[]).length;
if(assertCount>5){
issues.push({severity:'low',title:'Assert in Production',file:f.name,path:f.path,desc:assertCount+' assert statements found. Assertions are stripped with python -O. Use proper validation.',code:''});
}
}
// Hardcoded DEBUG = True
if(f.content.match(/\bDEBUG\s*=\s*True\b/)){
issues.push({severity:'medium',title:'Debug Mode Enabled',file:f.name,path:f.path,desc:'DEBUG = True found. Ensure this is disabled in production.',code:''});
}
}
});
return issues.sort(function(a,b){var sev={high:0,medium:1,low:2};return sev[a.severity]-sev[b.severity];});
},
// AST-based function extraction - accurate detection without false positives
extract:function(content,filename){
var fns=[];
var lines=content.split('\n');
// Helper to extract code snippet for a function
function extractCode(startLine,endLine){
var code=[];
var start=Math.max(0,startLine-1);
var end=Math.min(lines.length,endLine||startLine+20);
for(var i=start;i<end&&code.length<15;i++){
code.push(lines[i]);
}
if(code.length>=15)code.push(' // ...');
return code.join('\n');
}
// Track functions by line to allow same name at different locations
var seenAtLine={};
function addFn(fnObj){
var key=fnObj.name+'@'+fnObj.line;
if(!seenAtLine[key]){
seenAtLine[key]=true;
fns.push(fnObj);
}
}
// Check file type
var ext=filename.toLowerCase();
var isJS=ext.endsWith('.js')||ext.endsWith('.jsx')||ext.endsWith('.mjs')||ext.endsWith('.cjs');
var isTS=ext.endsWith('.ts')||ext.endsWith('.tsx');
var isVue=ext.endsWith('.vue');
var isSvelte=ext.endsWith('.svelte');
var isPython=ext.endsWith('.py')||ext.endsWith('.pyw')||ext.endsWith('.pyi');
// Extract script content from Vue/Svelte files
var scriptContent=content;
var scriptOffset=0;
if(isVue||isSvelte){
var scriptMatch=content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
if(scriptMatch){
scriptContent=scriptMatch[1];
scriptOffset=content.substring(0,content.indexOf(scriptMatch[1])).split('\n').length-1;
isJS=true; // Treat extracted script as JS
// Check if it's TypeScript
if(content.match(/<script[^>]*lang=["']ts["'][^>]*>/i)){
isTS=true;
isJS=false;
}
}else{
// No script tag found