toolnet-memory 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2987 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+
6
+ <meta
7
+ name="viewport"
8
+ content="width=device-width, initial-scale=1, viewport-fit=cover"
9
+ />
10
+
11
+ <title>
12
+ ToolNet Memory · Graph
13
+ </title>
14
+
15
+ <style>
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ html,
21
+ body {
22
+ width: 100%;
23
+ height: 100%;
24
+ margin: 0;
25
+ overflow: hidden;
26
+
27
+ background:
28
+ #050608;
29
+
30
+ color:
31
+ #f6f7f9;
32
+
33
+ font-family:
34
+ Inter,
35
+ ui-sans-serif,
36
+ system-ui,
37
+ -apple-system,
38
+ BlinkMacSystemFont,
39
+ "Segoe UI",
40
+ sans-serif;
41
+ }
42
+
43
+ #graph {
44
+ position: fixed;
45
+ inset: 0;
46
+ }
47
+
48
+ .panel {
49
+ position: fixed;
50
+ z-index: 20;
51
+
52
+ border:
53
+ 1px solid
54
+ rgba(255,255,255,.12);
55
+
56
+ background:
57
+ rgba(10,12,17,.88);
58
+
59
+ backdrop-filter:
60
+ blur(20px);
61
+
62
+ -webkit-backdrop-filter:
63
+ blur(20px);
64
+
65
+ box-shadow:
66
+ 0 22px 60px
67
+ rgba(0,0,0,.42);
68
+
69
+ border-radius:
70
+ 18px;
71
+ }
72
+
73
+ #toolbar {
74
+ top:
75
+ max(
76
+ 14px,
77
+ env(safe-area-inset-top)
78
+ );
79
+
80
+ left:
81
+ 14px;
82
+
83
+ right:
84
+ 14px;
85
+
86
+ padding:
87
+ 14px;
88
+
89
+ display:
90
+ grid;
91
+
92
+ grid-template-columns:
93
+ auto minmax(170px, 1fr)
94
+ minmax(170px, 1fr)
95
+ minmax(150px, .7fr)
96
+ minmax(150px, .7fr)
97
+ auto;
98
+
99
+ gap:
100
+ 10px;
101
+
102
+ align-items:
103
+ center;
104
+ }
105
+
106
+ .brand {
107
+ padding:
108
+ 0 8px;
109
+
110
+ white-space:
111
+ nowrap;
112
+
113
+ font-weight:
114
+ 750;
115
+
116
+ font-size:
117
+ 18px;
118
+ }
119
+
120
+ .brand span {
121
+ opacity:
122
+ .45;
123
+
124
+ font-weight:
125
+ 500;
126
+ }
127
+
128
+ input,
129
+ select,
130
+ button {
131
+ height:
132
+ 42px;
133
+
134
+ min-width:
135
+ 0;
136
+
137
+ border:
138
+ 1px solid
139
+ rgba(255,255,255,.12);
140
+
141
+ border-radius:
142
+ 11px;
143
+
144
+ background:
145
+ rgba(255,255,255,.06);
146
+
147
+ color:
148
+ white;
149
+
150
+ padding:
151
+ 0 12px;
152
+
153
+ outline:
154
+ none;
155
+
156
+ font:
157
+ inherit;
158
+ }
159
+
160
+ button {
161
+ cursor:
162
+ pointer;
163
+ }
164
+
165
+ #project {
166
+ font-weight:
167
+ 650;
168
+ }
169
+
170
+ #stats {
171
+ left:
172
+ 14px;
173
+
174
+ bottom:
175
+ max(
176
+ 14px,
177
+ env(safe-area-inset-bottom)
178
+ );
179
+
180
+ min-width:
181
+ 220px;
182
+
183
+ padding:
184
+ 14px 16px;
185
+
186
+ font-size:
187
+ 13px;
188
+
189
+ line-height:
190
+ 1.7;
191
+ }
192
+
193
+ .status {
194
+ margin-top:
195
+ 5px;
196
+
197
+ opacity:
198
+ .55;
199
+
200
+ font-size:
201
+ 11px;
202
+ }
203
+
204
+ #details {
205
+ top:
206
+ 96px;
207
+
208
+ right:
209
+ 14px;
210
+
211
+ bottom:
212
+ 14px;
213
+
214
+ width:
215
+ min(
216
+ 410px,
217
+ calc(100vw - 28px)
218
+ );
219
+
220
+ padding:
221
+ 68px 20px 20px;
222
+
223
+ overflow-y:
224
+ auto;
225
+
226
+ transform:
227
+ translateX(
228
+ calc(100% + 30px)
229
+ );
230
+
231
+ transition:
232
+ transform .2s ease;
233
+ }
234
+
235
+ #details.open {
236
+ transform:
237
+ translateX(0);
238
+ }
239
+
240
+ .details-close {
241
+ position:
242
+ absolute;
243
+
244
+ top:
245
+ 14px;
246
+
247
+ right:
248
+ 14px;
249
+
250
+ width:
251
+ 44px;
252
+
253
+ height:
254
+ 44px;
255
+
256
+ display:
257
+ grid;
258
+
259
+ place-items:
260
+ center;
261
+
262
+ padding:
263
+ 0;
264
+
265
+ border-radius:
266
+ 50%;
267
+
268
+ font-size:
269
+ 28px;
270
+
271
+ line-height:
272
+ 1;
273
+ }
274
+
275
+ .detail-title {
276
+ font-size:
277
+ 21px;
278
+
279
+ font-weight:
280
+ 750;
281
+
282
+ line-height:
283
+ 1.25;
284
+
285
+ overflow-wrap:
286
+ anywhere;
287
+ }
288
+
289
+ .path {
290
+ margin-top:
291
+ 8px;
292
+
293
+ opacity:
294
+ .55;
295
+
296
+ font-family:
297
+ ui-monospace,
298
+ SFMono-Regular,
299
+ Menlo,
300
+ monospace;
301
+
302
+ font-size:
303
+ 12px;
304
+
305
+ overflow-wrap:
306
+ anywhere;
307
+ }
308
+
309
+ .tag {
310
+ display:
311
+ inline-flex;
312
+
313
+ margin:
314
+ 10px 5px 0 0;
315
+
316
+ padding:
317
+ 5px 9px;
318
+
319
+ border:
320
+ 1px solid
321
+ rgba(255,255,255,.12);
322
+
323
+ border-radius:
324
+ 999px;
325
+
326
+ background:
327
+ rgba(255,255,255,.06);
328
+
329
+ font-size:
330
+ 11px;
331
+ }
332
+
333
+ .section {
334
+ margin-top:
335
+ 24px;
336
+ }
337
+
338
+ .section-title {
339
+ margin-bottom:
340
+ 8px;
341
+
342
+ opacity:
343
+ .5;
344
+
345
+ font-size:
346
+ 11px;
347
+
348
+ letter-spacing:
349
+ .1em;
350
+
351
+ text-transform:
352
+ uppercase;
353
+ }
354
+
355
+ .row {
356
+ display:
357
+ flex;
358
+
359
+ justify-content:
360
+ space-between;
361
+
362
+ gap:
363
+ 12px;
364
+
365
+ padding:
366
+ 9px 0;
367
+
368
+ border-bottom:
369
+ 1px solid
370
+ rgba(255,255,255,.06);
371
+
372
+ font-size:
373
+ 13px;
374
+ }
375
+
376
+ #loading {
377
+ position:
378
+ fixed;
379
+
380
+ inset:
381
+ 0;
382
+
383
+ z-index:
384
+ 100;
385
+
386
+ display:
387
+ grid;
388
+
389
+ place-items:
390
+ center;
391
+
392
+ background:
393
+ #050608;
394
+
395
+ padding:
396
+ 24px;
397
+
398
+ text-align:
399
+ center;
400
+ }
401
+
402
+ @media (
403
+ max-width: 1000px
404
+ ) {
405
+ #toolbar {
406
+ grid-template-columns:
407
+ 1fr 1fr;
408
+
409
+ top:
410
+ max(
411
+ 10px,
412
+ env(safe-area-inset-top)
413
+ );
414
+
415
+ left:
416
+ 10px;
417
+
418
+ right:
419
+ 10px;
420
+ }
421
+
422
+ .brand {
423
+ grid-column:
424
+ 1 / -1;
425
+
426
+ font-size:
427
+ 17px;
428
+ }
429
+
430
+ #search {
431
+ grid-column:
432
+ 1 / -1;
433
+ }
434
+
435
+ #details {
436
+ top:
437
+ 10px;
438
+
439
+ right:
440
+ 10px;
441
+
442
+ bottom:
443
+ 10px;
444
+ }
445
+
446
+ #stats {
447
+ left:
448
+ 10px;
449
+
450
+ bottom:
451
+ max(
452
+ 10px,
453
+ env(safe-area-inset-bottom)
454
+ );
455
+ }
456
+ }
457
+
458
+ @media (
459
+ max-width: 1000px
460
+ ) {
461
+ #toolbar {
462
+ grid-template-columns:
463
+ repeat(12, minmax(0, 1fr));
464
+ }
465
+
466
+ .brand {
467
+ grid-column:
468
+ 1 / -1;
469
+ }
470
+
471
+ #project {
472
+ grid-column:
473
+ 1 / 10;
474
+
475
+ width:
476
+ 100%;
477
+ }
478
+
479
+ #reset {
480
+ grid-column:
481
+ 10 / 13;
482
+
483
+ width:
484
+ 100%;
485
+
486
+ padding-left:
487
+ 8px;
488
+
489
+ padding-right:
490
+ 8px;
491
+ }
492
+
493
+ #search {
494
+ grid-column:
495
+ 1 / -1;
496
+
497
+ width:
498
+ 100%;
499
+ }
500
+
501
+ #cluster {
502
+ grid-column:
503
+ 1 / 7;
504
+
505
+ width:
506
+ 100%;
507
+ }
508
+
509
+ #edge {
510
+ grid-column:
511
+ 7 / 13;
512
+
513
+ width:
514
+ 100%;
515
+ }
516
+ }
517
+
518
+
519
+ .intel-summary {
520
+ display:
521
+ grid;
522
+
523
+ grid-template-columns:
524
+ 1fr 1fr;
525
+
526
+ gap:
527
+ 8px;
528
+
529
+ margin-top:
530
+ 18px;
531
+ }
532
+
533
+ .intel-card {
534
+ padding:
535
+ 12px;
536
+
537
+ border:
538
+ 1px solid
539
+ rgba(255,255,255,.08);
540
+
541
+ border-radius:
542
+ 12px;
543
+
544
+ background:
545
+ rgba(255,255,255,.035);
546
+ }
547
+
548
+ .intel-card strong {
549
+ display:
550
+ block;
551
+
552
+ font-size:
553
+ 18px;
554
+
555
+ margin-top:
556
+ 3px;
557
+ }
558
+
559
+ .intel-card span {
560
+ opacity:
561
+ .55;
562
+
563
+ font-size:
564
+ 11px;
565
+ }
566
+
567
+ .relation-list {
568
+ display:
569
+ flex;
570
+
571
+ flex-direction:
572
+ column;
573
+
574
+ gap:
575
+ 6px;
576
+ }
577
+
578
+ .relation-item {
579
+ width:
580
+ 100%;
581
+
582
+ height:
583
+ auto;
584
+
585
+ min-height:
586
+ 44px;
587
+
588
+ padding:
589
+ 9px 11px;
590
+
591
+ display:
592
+ block;
593
+
594
+ text-align:
595
+ left;
596
+
597
+ border-radius:
598
+ 10px;
599
+
600
+ cursor:
601
+ pointer;
602
+ }
603
+
604
+ .relation-name {
605
+ display:
606
+ block;
607
+
608
+ font-size:
609
+ 12px;
610
+
611
+ font-weight:
612
+ 650;
613
+
614
+ overflow-wrap:
615
+ anywhere;
616
+ }
617
+
618
+ .relation-meta {
619
+ display:
620
+ block;
621
+
622
+ margin-top:
623
+ 4px;
624
+
625
+ opacity:
626
+ .45;
627
+
628
+ font-size:
629
+ 10px;
630
+
631
+ font-family:
632
+ ui-monospace,
633
+ SFMono-Regular,
634
+ Menlo,
635
+ monospace;
636
+
637
+ overflow-wrap:
638
+ anywhere;
639
+ }
640
+
641
+ .relation-more {
642
+ margin-top:
643
+ 7px;
644
+
645
+ opacity:
646
+ .45;
647
+
648
+ font-size:
649
+ 11px;
650
+ }
651
+
652
+ .risk {
653
+ display:
654
+ inline-flex;
655
+
656
+ padding:
657
+ 5px 9px;
658
+
659
+ margin-top:
660
+ 8px;
661
+
662
+ border:
663
+ 1px solid
664
+ rgba(255,255,255,.12);
665
+
666
+ border-radius:
667
+ 999px;
668
+
669
+ font-size:
670
+ 11px;
671
+
672
+ font-weight:
673
+ 700;
674
+ }
675
+
676
+ .path-actions {
677
+ display:
678
+ flex;
679
+
680
+ gap:
681
+ 8px;
682
+
683
+ margin-top:
684
+ 10px;
685
+ }
686
+
687
+ .path-actions button {
688
+ flex:
689
+ 1;
690
+
691
+ height:
692
+ auto;
693
+
694
+ min-height:
695
+ 40px;
696
+
697
+ white-space:
698
+ normal;
699
+ }
700
+
701
+ .path-chain {
702
+ display:
703
+ flex;
704
+
705
+ flex-direction:
706
+ column;
707
+
708
+ gap:
709
+ 5px;
710
+
711
+ margin-top:
712
+ 9px;
713
+ }
714
+
715
+ .path-node {
716
+ height:
717
+ auto;
718
+
719
+ min-height:
720
+ 36px;
721
+
722
+ padding:
723
+ 7px 10px;
724
+
725
+ text-align:
726
+ left;
727
+
728
+ font-size:
729
+ 11px;
730
+ }
731
+
732
+ </style>
733
+ </head>
734
+
735
+ <body>
736
+ <div id="loading">
737
+ Loading ToolNet projects…
738
+ </div>
739
+
740
+ <div id="graph"></div>
741
+
742
+ <div
743
+ id="toolbar"
744
+ class="panel"
745
+ >
746
+ <div class="brand">
747
+ ToolNet Memory
748
+ <span>/ Graph</span>
749
+ </div>
750
+
751
+ <select
752
+ id="project"
753
+ aria-label="Project"
754
+ >
755
+ <option>
756
+ Loading…
757
+ </option>
758
+ </select>
759
+
760
+ <button id="reset">
761
+ Reset
762
+ </button>
763
+
764
+ <input
765
+ id="search"
766
+ placeholder="Search symbol or file…"
767
+ />
768
+
769
+ <select
770
+ id="cluster"
771
+ aria-label="Subsystem"
772
+ >
773
+ <option value="">
774
+ All subsystems
775
+ </option>
776
+ </select>
777
+
778
+ <select
779
+ id="edge"
780
+ aria-label="Relation"
781
+ >
782
+ <option value="">
783
+ All relations
784
+ </option>
785
+ </select>
786
+
787
+ </div>
788
+
789
+ <div
790
+ id="stats"
791
+ class="panel"
792
+ ></div>
793
+
794
+ <div
795
+ id="details"
796
+ class="panel"
797
+ ></div>
798
+
799
+ <script src="/vendor/3d-force-graph.min.js"></script>
800
+
801
+ <script>
802
+ const state = {
803
+ raw:
804
+ null,
805
+
806
+ graph:
807
+ null,
808
+
809
+ project:
810
+ "",
811
+
812
+ search:
813
+ "",
814
+
815
+ cluster:
816
+ "",
817
+
818
+ edge:
819
+ "",
820
+
821
+ selected:
822
+ null,
823
+
824
+ pathStart:
825
+ null,
826
+
827
+ highlightedPath:
828
+ [],
829
+
830
+ viewMode:
831
+ "overview",
832
+
833
+ viewCluster:
834
+ "",
835
+
836
+ viewFile:
837
+ "",
838
+ };
839
+
840
+ const graphEl =
841
+ document.getElementById(
842
+ "graph"
843
+ );
844
+
845
+ const projectEl =
846
+ document.getElementById(
847
+ "project"
848
+ );
849
+
850
+ const searchEl =
851
+ document.getElementById(
852
+ "search"
853
+ );
854
+
855
+ const clusterEl =
856
+ document.getElementById(
857
+ "cluster"
858
+ );
859
+
860
+ const edgeEl =
861
+ document.getElementById(
862
+ "edge"
863
+ );
864
+
865
+ const statsEl =
866
+ document.getElementById(
867
+ "stats"
868
+ );
869
+
870
+ const detailsEl =
871
+ document.getElementById(
872
+ "details"
873
+ );
874
+
875
+ const colors = [
876
+ "#62a8ff",
877
+ "#ff8f6b",
878
+ "#67d49b",
879
+ "#c493ff",
880
+ "#ffd166",
881
+ "#73d2de",
882
+ "#ff6b9a",
883
+ "#9fd356",
884
+ "#e5989b",
885
+ "#8ecae6",
886
+ "#c9cba3",
887
+ "#f4a261",
888
+ ];
889
+
890
+ function escapeHtml(
891
+ value
892
+ ) {
893
+ return String(
894
+ value ?? ""
895
+ )
896
+ .replaceAll(
897
+ "&",
898
+ "&amp;"
899
+ )
900
+ .replaceAll(
901
+ "<",
902
+ "&lt;"
903
+ )
904
+ .replaceAll(
905
+ ">",
906
+ "&gt;"
907
+ )
908
+ .replaceAll(
909
+ '"',
910
+ "&quot;"
911
+ )
912
+ .replaceAll(
913
+ "'",
914
+ "&#039;"
915
+ );
916
+ }
917
+
918
+ function hashCode(
919
+ value
920
+ ) {
921
+ let hash =
922
+ 0;
923
+
924
+ for (
925
+ let i = 0;
926
+ i < value.length;
927
+ i++
928
+ ) {
929
+ hash =
930
+ (
931
+ (
932
+ hash << 5
933
+ ) -
934
+ hash
935
+ ) +
936
+ value.charCodeAt(
937
+ i
938
+ );
939
+
940
+ hash |=
941
+ 0;
942
+ }
943
+
944
+ return Math.abs(
945
+ hash
946
+ );
947
+ }
948
+
949
+ function nodeColor(
950
+ node
951
+ ) {
952
+ const value =
953
+ node.clusterLabel ||
954
+ node.cluster ||
955
+ node.layer ||
956
+ node.type ||
957
+ "other";
958
+
959
+ return colors[
960
+ hashCode(
961
+ value
962
+ ) %
963
+ colors.length
964
+ ];
965
+ }
966
+
967
+ function linkColor(
968
+ link
969
+ ) {
970
+ const map = {
971
+ CALL_REFERENCE:
972
+ "rgba(255,190,90,.72)",
973
+
974
+ CALLS:
975
+ "rgba(255,255,255,.30)",
976
+
977
+ IMPORTS:
978
+ "rgba(100,170,255,.46)",
979
+
980
+ USES_TYPE:
981
+ "rgba(190,130,255,.44)",
982
+
983
+ WRITES:
984
+ "rgba(255,100,100,.60)",
985
+
986
+ TESTS:
987
+ "rgba(100,220,150,.42)",
988
+
989
+ IMPLEMENTS:
990
+ "rgba(100,220,220,.48)",
991
+
992
+ INHERITS:
993
+ "rgba(235,140,255,.50)",
994
+ };
995
+
996
+ return (
997
+ map[link.type] ||
998
+ "rgba(255,255,255,.18)"
999
+ );
1000
+ }
1001
+
1002
+ function closeDetails() {
1003
+ state.selected =
1004
+ null;
1005
+
1006
+ detailsEl
1007
+ .classList
1008
+ .remove(
1009
+ "open"
1010
+ );
1011
+
1012
+ detailsEl.innerHTML =
1013
+ "";
1014
+ }
1015
+
1016
+ const CALL_RELATIONS =
1017
+ new Set([
1018
+ "CALLS",
1019
+ "CALL_REFERENCE",
1020
+ ]);
1021
+
1022
+ const DEPENDENCY_RELATIONS =
1023
+ new Set([
1024
+ "CALLS",
1025
+ "CALL_REFERENCE",
1026
+ "IMPORTS",
1027
+ "USES_TYPE",
1028
+ "WRITES",
1029
+ "INHERITS",
1030
+ "IMPLEMENTS",
1031
+ "ROUTE",
1032
+ ]);
1033
+
1034
+ const IMPACT_WEIGHTS = {
1035
+ CALLS: 4,
1036
+ CALL_REFERENCE: 6,
1037
+ USES_TYPE: 4,
1038
+ TESTS: 2,
1039
+ ROUTE: 4,
1040
+ WRITES: 3,
1041
+ IMPORTS: 3,
1042
+ INHERITS: 5,
1043
+ IMPLEMENTS: 5,
1044
+ DEFINES: 1,
1045
+ };
1046
+
1047
+ function endpointId(
1048
+ link,
1049
+ side
1050
+ ) {
1051
+ const value =
1052
+ link[side];
1053
+
1054
+ return typeof value ===
1055
+ "object"
1056
+ ? value.id
1057
+ : value;
1058
+ }
1059
+
1060
+ function rawNode(
1061
+ id
1062
+ ) {
1063
+ return state.raw
1064
+ ?.nodes
1065
+ ?.find(
1066
+ node =>
1067
+ node.id === id
1068
+ );
1069
+ }
1070
+
1071
+ function uniqueNodes(
1072
+ values
1073
+ ) {
1074
+ const map =
1075
+ new Map();
1076
+
1077
+ for (
1078
+ const node
1079
+ of values
1080
+ ) {
1081
+ if (
1082
+ node &&
1083
+ !map.has(
1084
+ node.id
1085
+ )
1086
+ ) {
1087
+ map.set(
1088
+ node.id,
1089
+ node
1090
+ );
1091
+ }
1092
+ }
1093
+
1094
+ return [
1095
+ ...map.values()
1096
+ ];
1097
+ }
1098
+
1099
+ function relatedNodes(
1100
+ nodeId,
1101
+ direction,
1102
+ allowed
1103
+ ) {
1104
+ const result =
1105
+ [];
1106
+
1107
+ for (
1108
+ const link
1109
+ of state.raw.links
1110
+ ) {
1111
+ if (
1112
+ allowed &&
1113
+ !allowed.has(
1114
+ link.type
1115
+ )
1116
+ ) {
1117
+ continue;
1118
+ }
1119
+
1120
+ const source =
1121
+ endpointId(
1122
+ link,
1123
+ "source"
1124
+ );
1125
+
1126
+ const target =
1127
+ endpointId(
1128
+ link,
1129
+ "target"
1130
+ );
1131
+
1132
+ if (
1133
+ direction ===
1134
+ "incoming" &&
1135
+ target ===
1136
+ nodeId
1137
+ ) {
1138
+ const node =
1139
+ rawNode(
1140
+ source
1141
+ );
1142
+
1143
+ if (node) {
1144
+ result.push(
1145
+ node
1146
+ );
1147
+ }
1148
+ }
1149
+
1150
+ if (
1151
+ direction ===
1152
+ "outgoing" &&
1153
+ source ===
1154
+ nodeId
1155
+ ) {
1156
+ const node =
1157
+ rawNode(
1158
+ target
1159
+ );
1160
+
1161
+ if (node) {
1162
+ result.push(
1163
+ node
1164
+ );
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ return uniqueNodes(
1170
+ result
1171
+ );
1172
+ }
1173
+
1174
+ function blastRadius(
1175
+ startId,
1176
+ maxDepth = 3
1177
+ ) {
1178
+ const visited =
1179
+ new Map();
1180
+
1181
+ const queue = [
1182
+ {
1183
+ id:
1184
+ startId,
1185
+
1186
+ depth:
1187
+ 0,
1188
+
1189
+ score:
1190
+ 0,
1191
+ }
1192
+ ];
1193
+
1194
+ visited.set(
1195
+ startId,
1196
+ {
1197
+ depth:
1198
+ 0,
1199
+
1200
+ score:
1201
+ 0,
1202
+ }
1203
+ );
1204
+
1205
+ while (
1206
+ queue.length
1207
+ ) {
1208
+ const current =
1209
+ queue.shift();
1210
+
1211
+ if (
1212
+ current.depth >=
1213
+ maxDepth
1214
+ ) {
1215
+ continue;
1216
+ }
1217
+
1218
+ for (
1219
+ const link
1220
+ of state.raw.links
1221
+ ) {
1222
+ const weight =
1223
+ IMPACT_WEIGHTS[
1224
+ link.type
1225
+ ];
1226
+
1227
+ if (!weight) {
1228
+ continue;
1229
+ }
1230
+
1231
+ const source =
1232
+ endpointId(
1233
+ link,
1234
+ "source"
1235
+ );
1236
+
1237
+ const target =
1238
+ endpointId(
1239
+ link,
1240
+ "target"
1241
+ );
1242
+
1243
+ /*
1244
+ * Reverse traversal:
1245
+ * if source depends on target,
1246
+ * changing target may affect source.
1247
+ */
1248
+ if (
1249
+ target !==
1250
+ current.id
1251
+ ) {
1252
+ continue;
1253
+ }
1254
+
1255
+ const nextDepth =
1256
+ current.depth +
1257
+ 1;
1258
+
1259
+ const nextScore =
1260
+ current.score +
1261
+ weight *
1262
+ (
1263
+ maxDepth -
1264
+ current.depth
1265
+ );
1266
+
1267
+ const previous =
1268
+ visited.get(
1269
+ source
1270
+ );
1271
+
1272
+ if (
1273
+ !previous ||
1274
+ nextDepth <
1275
+ previous.depth ||
1276
+ nextScore >
1277
+ previous.score
1278
+ ) {
1279
+ visited.set(
1280
+ source,
1281
+ {
1282
+ depth:
1283
+ nextDepth,
1284
+
1285
+ score:
1286
+ nextScore,
1287
+ }
1288
+ );
1289
+
1290
+ queue.push({
1291
+ id:
1292
+ source,
1293
+
1294
+ depth:
1295
+ nextDepth,
1296
+
1297
+ score:
1298
+ nextScore,
1299
+ });
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ visited.delete(
1305
+ startId
1306
+ );
1307
+
1308
+ const impacted =
1309
+ [
1310
+ ...visited.entries()
1311
+ ]
1312
+ .map(
1313
+ ([id, info]) => ({
1314
+ node:
1315
+ rawNode(
1316
+ id
1317
+ ),
1318
+
1319
+ ...info,
1320
+ })
1321
+ )
1322
+ .filter(
1323
+ item =>
1324
+ item.node
1325
+ )
1326
+ .sort(
1327
+ (a, b) =>
1328
+ b.score -
1329
+ a.score
1330
+ );
1331
+
1332
+ const score =
1333
+ impacted.reduce(
1334
+ (
1335
+ total,
1336
+ item
1337
+ ) =>
1338
+ total +
1339
+ item.score,
1340
+ 0
1341
+ );
1342
+
1343
+ let risk =
1344
+ "LOW";
1345
+
1346
+ if (
1347
+ score >= 250 ||
1348
+ impacted.length >= 40
1349
+ ) {
1350
+ risk =
1351
+ "CRITICAL";
1352
+ } else if (
1353
+ score >= 120 ||
1354
+ impacted.length >= 20
1355
+ ) {
1356
+ risk =
1357
+ "HIGH";
1358
+ } else if (
1359
+ score >= 40 ||
1360
+ impacted.length >= 8
1361
+ ) {
1362
+ risk =
1363
+ "MEDIUM";
1364
+ }
1365
+
1366
+ return {
1367
+ impacted,
1368
+ score,
1369
+ risk,
1370
+ };
1371
+ }
1372
+
1373
+ function shortestPath(
1374
+ fromId,
1375
+ toId,
1376
+ maxDepth = 10
1377
+ ) {
1378
+ if (
1379
+ fromId ===
1380
+ toId
1381
+ ) {
1382
+ return [
1383
+ fromId
1384
+ ];
1385
+ }
1386
+
1387
+ const queue = [
1388
+ {
1389
+ id:
1390
+ fromId,
1391
+
1392
+ path: [
1393
+ fromId
1394
+ ],
1395
+ }
1396
+ ];
1397
+
1398
+ const visited =
1399
+ new Set([
1400
+ fromId
1401
+ ]);
1402
+
1403
+ while (
1404
+ queue.length
1405
+ ) {
1406
+ const current =
1407
+ queue.shift();
1408
+
1409
+ if (
1410
+ current.path.length >
1411
+ maxDepth
1412
+ ) {
1413
+ continue;
1414
+ }
1415
+
1416
+ for (
1417
+ const link
1418
+ of state.raw.links
1419
+ ) {
1420
+ if (
1421
+ !DEPENDENCY_RELATIONS
1422
+ .has(
1423
+ link.type
1424
+ )
1425
+ ) {
1426
+ continue;
1427
+ }
1428
+
1429
+ if (
1430
+ endpointId(
1431
+ link,
1432
+ "source"
1433
+ ) !==
1434
+ current.id
1435
+ ) {
1436
+ continue;
1437
+ }
1438
+
1439
+ const next =
1440
+ endpointId(
1441
+ link,
1442
+ "target"
1443
+ );
1444
+
1445
+ if (
1446
+ visited.has(
1447
+ next
1448
+ )
1449
+ ) {
1450
+ continue;
1451
+ }
1452
+
1453
+ const path = [
1454
+ ...current.path,
1455
+ next,
1456
+ ];
1457
+
1458
+ if (
1459
+ next ===
1460
+ toId
1461
+ ) {
1462
+ return path;
1463
+ }
1464
+
1465
+ visited.add(
1466
+ next
1467
+ );
1468
+
1469
+ queue.push({
1470
+ id:
1471
+ next,
1472
+
1473
+ path,
1474
+ });
1475
+ }
1476
+ }
1477
+
1478
+ return null;
1479
+ }
1480
+
1481
+ function relationHtml(
1482
+ title,
1483
+ nodes,
1484
+ limit = 8
1485
+ ) {
1486
+ const visible =
1487
+ nodes.slice(
1488
+ 0,
1489
+ limit
1490
+ );
1491
+
1492
+ return `
1493
+ <div class="section">
1494
+ <div class="section-title">
1495
+ ${escapeHtml(
1496
+ title
1497
+ )}
1498
+ · ${nodes.length}
1499
+ </div>
1500
+
1501
+ ${
1502
+ visible.length
1503
+ ? `
1504
+ <div class="relation-list">
1505
+ ${
1506
+ visible
1507
+ .map(
1508
+ node => `
1509
+ <button
1510
+ class="relation-item"
1511
+ data-jump-node="${escapeHtml(
1512
+ node.id
1513
+ )}"
1514
+ >
1515
+ <span class="relation-name">
1516
+ ${escapeHtml(
1517
+ node.qualifiedName ||
1518
+ node.name
1519
+ )}
1520
+ </span>
1521
+
1522
+ <span class="relation-meta">
1523
+ ${escapeHtml(
1524
+ node.filePath
1525
+ )}
1526
+ </span>
1527
+ </button>
1528
+ `
1529
+ )
1530
+ .join("")
1531
+ }
1532
+ </div>
1533
+ `
1534
+ : `
1535
+ <div class="muted">
1536
+ None
1537
+ </div>
1538
+ `
1539
+ }
1540
+
1541
+ ${
1542
+ nodes.length >
1543
+ limit
1544
+ ? `
1545
+ <div class="relation-more">
1546
+ +${
1547
+ nodes.length -
1548
+ limit
1549
+ } more
1550
+ </div>
1551
+ `
1552
+ : ""
1553
+ }
1554
+ </div>
1555
+ `;
1556
+ }
1557
+
1558
+ function resetPathHighlight() {
1559
+ state.highlightedPath =
1560
+ [];
1561
+
1562
+ if (
1563
+ !state.graph
1564
+ ) {
1565
+ return;
1566
+ }
1567
+
1568
+ state.graph
1569
+ .linkWidth(
1570
+ link =>
1571
+ link.type ===
1572
+ "CALL_REFERENCE"
1573
+ ? 1.2
1574
+ : .45
1575
+ )
1576
+ .linkColor(
1577
+ link =>
1578
+ linkColor(
1579
+ link
1580
+ )
1581
+ );
1582
+ }
1583
+
1584
+ function isPathLink(
1585
+ link,
1586
+ path
1587
+ ) {
1588
+ const source =
1589
+ endpointId(
1590
+ link,
1591
+ "source"
1592
+ );
1593
+
1594
+ const target =
1595
+ endpointId(
1596
+ link,
1597
+ "target"
1598
+ );
1599
+
1600
+ for (
1601
+ let i = 0;
1602
+ i <
1603
+ path.length - 1;
1604
+ i++
1605
+ ) {
1606
+ if (
1607
+ path[i] ===
1608
+ source &&
1609
+ path[i + 1] ===
1610
+ target
1611
+ ) {
1612
+ return true;
1613
+ }
1614
+ }
1615
+
1616
+ return false;
1617
+ }
1618
+
1619
+ function highlightPath(
1620
+ path
1621
+ ) {
1622
+ state.highlightedPath =
1623
+ path || [];
1624
+
1625
+ if (
1626
+ !state.graph
1627
+ ) {
1628
+ return;
1629
+ }
1630
+
1631
+ state.graph
1632
+ .linkWidth(
1633
+ link =>
1634
+ isPathLink(
1635
+ link,
1636
+ state.highlightedPath
1637
+ )
1638
+ ? 3.5
1639
+ : (
1640
+ link.type ===
1641
+ "CALL_REFERENCE"
1642
+ ? 1.2
1643
+ : .35
1644
+ )
1645
+ )
1646
+ .linkColor(
1647
+ link =>
1648
+ isPathLink(
1649
+ link,
1650
+ state.highlightedPath
1651
+ )
1652
+ ? "rgba(255,225,80,.98)"
1653
+ : linkColor(
1654
+ link
1655
+ )
1656
+ );
1657
+ }
1658
+
1659
+ function flyToNode(
1660
+ node
1661
+ ) {
1662
+ const distance =
1663
+ 70;
1664
+
1665
+ const length =
1666
+ Math.hypot(
1667
+ node.x || 1,
1668
+ node.y || 1,
1669
+ node.z || 1
1670
+ );
1671
+
1672
+ const ratio =
1673
+ 1 +
1674
+ distance /
1675
+ length;
1676
+
1677
+ state.graph
1678
+ .cameraPosition(
1679
+ {
1680
+ x:
1681
+ node.x *
1682
+ ratio,
1683
+
1684
+ y:
1685
+ node.y *
1686
+ ratio,
1687
+
1688
+ z:
1689
+ node.z *
1690
+ ratio,
1691
+ },
1692
+
1693
+ node,
1694
+
1695
+ 800
1696
+ );
1697
+ }
1698
+
1699
+ function jumpToNode(
1700
+ id
1701
+ ) {
1702
+ /*
1703
+ * Clear filters if destination is hidden.
1704
+ */
1705
+ searchEl.value =
1706
+ "";
1707
+
1708
+ clusterEl.value =
1709
+ "";
1710
+
1711
+ edgeEl.value =
1712
+ "";
1713
+
1714
+ state.search =
1715
+ "";
1716
+
1717
+ state.cluster =
1718
+ "";
1719
+
1720
+ state.edge =
1721
+ "";
1722
+
1723
+ applyFilters();
1724
+
1725
+ setTimeout(
1726
+ () => {
1727
+ const node =
1728
+ state.graph
1729
+ .graphData()
1730
+ .nodes
1731
+ .find(
1732
+ item =>
1733
+ item.id ===
1734
+ id
1735
+ );
1736
+
1737
+ if (!node) {
1738
+ return;
1739
+ }
1740
+
1741
+ showDetails(
1742
+ node
1743
+ );
1744
+
1745
+ flyToNode(
1746
+ node
1747
+ );
1748
+ },
1749
+ 100
1750
+ );
1751
+ }
1752
+
1753
+ function showDetails(
1754
+ node
1755
+ ) {
1756
+ if (
1757
+ node.viewKind ===
1758
+ "cluster"
1759
+ ) {
1760
+ void loadProject(
1761
+ state.project,
1762
+ {
1763
+ mode:
1764
+ "files",
1765
+
1766
+ cluster:
1767
+ node.clusterLabel ||
1768
+ node.name,
1769
+ }
1770
+ );
1771
+
1772
+ return;
1773
+ }
1774
+
1775
+ if (
1776
+ node.viewKind ===
1777
+ "file"
1778
+ ) {
1779
+ void loadProject(
1780
+ state.project,
1781
+ {
1782
+ mode:
1783
+ "symbols",
1784
+
1785
+ file:
1786
+ node.filePath,
1787
+ }
1788
+ );
1789
+
1790
+ return;
1791
+ }
1792
+
1793
+ state.selected =
1794
+ node;
1795
+
1796
+ detailsEl
1797
+ .classList
1798
+ .add(
1799
+ "open"
1800
+ );
1801
+
1802
+ const callers =
1803
+ relatedNodes(
1804
+ node.id,
1805
+ "incoming",
1806
+ CALL_RELATIONS
1807
+ );
1808
+
1809
+ const callees =
1810
+ relatedNodes(
1811
+ node.id,
1812
+ "outgoing",
1813
+ CALL_RELATIONS
1814
+ );
1815
+
1816
+ const dependencies =
1817
+ relatedNodes(
1818
+ node.id,
1819
+ "outgoing",
1820
+ DEPENDENCY_RELATIONS
1821
+ );
1822
+
1823
+ const dependents =
1824
+ relatedNodes(
1825
+ node.id,
1826
+ "incoming",
1827
+ DEPENDENCY_RELATIONS
1828
+ );
1829
+
1830
+ const blast =
1831
+ blastRadius(
1832
+ node.id,
1833
+ 3
1834
+ );
1835
+
1836
+ let path =
1837
+ null;
1838
+
1839
+ if (
1840
+ state.pathStart &&
1841
+ state.pathStart !==
1842
+ node.id
1843
+ ) {
1844
+ path =
1845
+ shortestPath(
1846
+ state.pathStart,
1847
+ node.id
1848
+ );
1849
+
1850
+ if (path) {
1851
+ highlightPath(
1852
+ path
1853
+ );
1854
+ } else {
1855
+ resetPathHighlight();
1856
+ }
1857
+ }
1858
+
1859
+ const pathStartNode =
1860
+ state.pathStart
1861
+ ? rawNode(
1862
+ state.pathStart
1863
+ )
1864
+ : null;
1865
+
1866
+ detailsEl.innerHTML = `
1867
+ <button
1868
+ class="details-close"
1869
+ id="details-close"
1870
+ type="button"
1871
+ aria-label="Close"
1872
+ >
1873
+ &times;
1874
+ </button>
1875
+
1876
+ <div class="detail-title">
1877
+ ${escapeHtml(
1878
+ node.qualifiedName ||
1879
+ node.name
1880
+ )}
1881
+ </div>
1882
+
1883
+ <div class="path">
1884
+ ${escapeHtml(
1885
+ node.filePath
1886
+ )}
1887
+ </div>
1888
+
1889
+ <div>
1890
+ <span class="tag">
1891
+ ${escapeHtml(
1892
+ node.type
1893
+ )}
1894
+ </span>
1895
+
1896
+ ${
1897
+ node.clusterLabel
1898
+ ? `
1899
+ <span class="tag">
1900
+ ${escapeHtml(
1901
+ node.clusterLabel
1902
+ )}
1903
+ </span>
1904
+ `
1905
+ : ""
1906
+ }
1907
+
1908
+ ${
1909
+ node.layer
1910
+ ? `
1911
+ <span class="tag">
1912
+ ${escapeHtml(
1913
+ node.layer
1914
+ )}
1915
+ </span>
1916
+ `
1917
+ : ""
1918
+ }
1919
+
1920
+ ${
1921
+ node.deadCode
1922
+ ? `
1923
+ <span class="tag">
1924
+ dead-code:
1925
+ ${escapeHtml(
1926
+ node.deadCode
1927
+ .confidence
1928
+ )}
1929
+ </span>
1930
+ `
1931
+ : ""
1932
+ }
1933
+ </div>
1934
+
1935
+ <div class="intel-summary">
1936
+ <div class="intel-card">
1937
+ <span>Callers</span>
1938
+ <strong>
1939
+ ${callers.length}
1940
+ </strong>
1941
+ </div>
1942
+
1943
+ <div class="intel-card">
1944
+ <span>Callees</span>
1945
+ <strong>
1946
+ ${callees.length}
1947
+ </strong>
1948
+ </div>
1949
+
1950
+ <div class="intel-card">
1951
+ <span>Dependents</span>
1952
+ <strong>
1953
+ ${dependents.length}
1954
+ </strong>
1955
+ </div>
1956
+
1957
+ <div class="intel-card">
1958
+ <span>Dependencies</span>
1959
+ <strong>
1960
+ ${dependencies.length}
1961
+ </strong>
1962
+ </div>
1963
+ </div>
1964
+
1965
+ <div class="section">
1966
+ <div class="section-title">
1967
+ Blast Radius
1968
+ </div>
1969
+
1970
+ <div class="row">
1971
+ <span>Risk</span>
1972
+ <strong>
1973
+ ${blast.risk}
1974
+ </strong>
1975
+ </div>
1976
+
1977
+ <div class="row">
1978
+ <span>Impacted nodes</span>
1979
+ <strong>
1980
+ ${blast.impacted.length}
1981
+ </strong>
1982
+ </div>
1983
+
1984
+ <div class="row">
1985
+ <span>Impact score</span>
1986
+ <strong>
1987
+ ${blast.score}
1988
+ </strong>
1989
+ </div>
1990
+
1991
+ <span class="risk">
1992
+ ${blast.risk}
1993
+ </span>
1994
+ </div>
1995
+
1996
+ ${
1997
+ blast.impacted.length
1998
+ ? relationHtml(
1999
+ "Top Impacted",
2000
+ blast.impacted
2001
+ .slice(
2002
+ 0,
2003
+ 8
2004
+ )
2005
+ .map(
2006
+ item =>
2007
+ item.node
2008
+ )
2009
+ )
2010
+ : ""
2011
+ }
2012
+
2013
+ ${relationHtml(
2014
+ "Callers",
2015
+ callers
2016
+ )}
2017
+
2018
+ ${relationHtml(
2019
+ "Callees",
2020
+ callees
2021
+ )}
2022
+
2023
+ ${relationHtml(
2024
+ "Dependencies",
2025
+ dependencies
2026
+ )}
2027
+
2028
+ ${relationHtml(
2029
+ "Dependents",
2030
+ dependents
2031
+ )}
2032
+
2033
+ <div class="section">
2034
+ <div class="section-title">
2035
+ Shortest Path
2036
+ </div>
2037
+
2038
+ ${
2039
+ !state.pathStart
2040
+ ? `
2041
+ <div class="muted">
2042
+ Chọn node này làm điểm bắt đầu,
2043
+ sau đó click node khác.
2044
+ </div>
2045
+ `
2046
+ : `
2047
+ <div class="path">
2048
+ From:
2049
+ ${escapeHtml(
2050
+ pathStartNode
2051
+ ?.qualifiedName ||
2052
+ pathStartNode
2053
+ ?.name ||
2054
+ state.pathStart
2055
+ )}
2056
+ </div>
2057
+ `
2058
+ }
2059
+
2060
+ <div class="path-actions">
2061
+ <button
2062
+ id="path-start"
2063
+ type="button"
2064
+ >
2065
+ ${
2066
+ state.pathStart ===
2067
+ node.id
2068
+ ? "Start selected"
2069
+ : "Set path start"
2070
+ }
2071
+ </button>
2072
+
2073
+ ${
2074
+ state.pathStart
2075
+ ? `
2076
+ <button
2077
+ id="path-clear"
2078
+ type="button"
2079
+ >
2080
+ Clear path
2081
+ </button>
2082
+ `
2083
+ : ""
2084
+ }
2085
+ </div>
2086
+
2087
+ ${
2088
+ state.pathStart &&
2089
+ state.pathStart !==
2090
+ node.id
2091
+ ? (
2092
+ path
2093
+ ? `
2094
+ <div class="path-chain">
2095
+ ${
2096
+ path.map(
2097
+ id => {
2098
+ const item =
2099
+ rawNode(
2100
+ id
2101
+ );
2102
+
2103
+ return `
2104
+ <button
2105
+ class="path-node"
2106
+ data-jump-node="${escapeHtml(
2107
+ id
2108
+ )}"
2109
+ >
2110
+ ${escapeHtml(
2111
+ item?.qualifiedName ||
2112
+ item?.name ||
2113
+ id
2114
+ )}
2115
+ </button>
2116
+ `;
2117
+ }
2118
+ ).join("")
2119
+ }
2120
+ </div>
2121
+ `
2122
+ : `
2123
+ <div class="relation-more">
2124
+ Không tìm thấy directed path
2125
+ trong 10 bước.
2126
+ </div>
2127
+ `
2128
+ )
2129
+ : ""
2130
+ }
2131
+ </div>
2132
+ `;
2133
+
2134
+ document
2135
+ .getElementById(
2136
+ "details-close"
2137
+ )
2138
+ ?.addEventListener(
2139
+ "click",
2140
+ event => {
2141
+ event.stopPropagation();
2142
+ closeDetails();
2143
+ }
2144
+ );
2145
+
2146
+ detailsEl
2147
+ .querySelectorAll(
2148
+ "[data-jump-node]"
2149
+ )
2150
+ .forEach(
2151
+ element => {
2152
+ element
2153
+ .addEventListener(
2154
+ "click",
2155
+ event => {
2156
+ event.stopPropagation();
2157
+
2158
+ jumpToNode(
2159
+ element.dataset
2160
+ .jumpNode
2161
+ );
2162
+ }
2163
+ );
2164
+ }
2165
+ );
2166
+
2167
+ document
2168
+ .getElementById(
2169
+ "path-start"
2170
+ )
2171
+ ?.addEventListener(
2172
+ "click",
2173
+ event => {
2174
+ event.stopPropagation();
2175
+
2176
+ state.pathStart =
2177
+ node.id;
2178
+
2179
+ resetPathHighlight();
2180
+
2181
+ showDetails(
2182
+ node
2183
+ );
2184
+ }
2185
+ );
2186
+
2187
+ document
2188
+ .getElementById(
2189
+ "path-clear"
2190
+ )
2191
+ ?.addEventListener(
2192
+ "click",
2193
+ event => {
2194
+ event.stopPropagation();
2195
+
2196
+ state.pathStart =
2197
+ null;
2198
+
2199
+ resetPathHighlight();
2200
+
2201
+ showDetails(
2202
+ node
2203
+ );
2204
+ }
2205
+ );
2206
+ }
2207
+
2208
+ function populateFilters() {
2209
+ clusterEl.innerHTML =
2210
+ '<option value="">All subsystems</option>';
2211
+
2212
+ edgeEl.innerHTML =
2213
+ '<option value="">All relations</option>';
2214
+
2215
+ const clusters =
2216
+ [
2217
+ ...new Set(
2218
+ state.raw.nodes
2219
+ .map(
2220
+ node =>
2221
+ node.clusterLabel
2222
+ )
2223
+ .filter(Boolean)
2224
+ )
2225
+ ]
2226
+ .sort();
2227
+
2228
+ for (
2229
+ const value
2230
+ of clusters
2231
+ ) {
2232
+ const option =
2233
+ document.createElement(
2234
+ "option"
2235
+ );
2236
+
2237
+ option.value =
2238
+ value;
2239
+
2240
+ option.textContent =
2241
+ value;
2242
+
2243
+ clusterEl.appendChild(
2244
+ option
2245
+ );
2246
+ }
2247
+
2248
+ const edges =
2249
+ [
2250
+ ...new Set(
2251
+ state.raw.links
2252
+ .map(
2253
+ link =>
2254
+ link.type
2255
+ )
2256
+ )
2257
+ ]
2258
+ .sort();
2259
+
2260
+ for (
2261
+ const value
2262
+ of edges
2263
+ ) {
2264
+ const option =
2265
+ document.createElement(
2266
+ "option"
2267
+ );
2268
+
2269
+ option.value =
2270
+ value;
2271
+
2272
+ option.textContent =
2273
+ value;
2274
+
2275
+ edgeEl.appendChild(
2276
+ option
2277
+ );
2278
+ }
2279
+ }
2280
+
2281
+ function filteredData() {
2282
+ const search =
2283
+ state.search
2284
+ .trim()
2285
+ .toLowerCase();
2286
+
2287
+ let nodes =
2288
+ state.raw.nodes;
2289
+
2290
+ if (
2291
+ state.cluster
2292
+ ) {
2293
+ nodes =
2294
+ nodes.filter(
2295
+ node =>
2296
+ node.clusterLabel ===
2297
+ state.cluster
2298
+ );
2299
+ }
2300
+
2301
+ if (
2302
+ search
2303
+ ) {
2304
+ nodes =
2305
+ nodes.filter(
2306
+ node =>
2307
+ node.name
2308
+ ?.toLowerCase()
2309
+ .includes(
2310
+ search
2311
+ ) ||
2312
+
2313
+ node.qualifiedName
2314
+ ?.toLowerCase()
2315
+ .includes(
2316
+ search
2317
+ ) ||
2318
+
2319
+ node.filePath
2320
+ ?.toLowerCase()
2321
+ .includes(
2322
+ search
2323
+ )
2324
+ );
2325
+ }
2326
+
2327
+ const ids =
2328
+ new Set(
2329
+ nodes.map(
2330
+ node =>
2331
+ node.id
2332
+ )
2333
+ );
2334
+
2335
+ let links =
2336
+ state.raw.links
2337
+ .filter(
2338
+ link => {
2339
+ const source =
2340
+ typeof link.source ===
2341
+ "object"
2342
+ ? link.source.id
2343
+ : link.source;
2344
+
2345
+ const target =
2346
+ typeof link.target ===
2347
+ "object"
2348
+ ? link.target.id
2349
+ : link.target;
2350
+
2351
+ return (
2352
+ ids.has(
2353
+ source
2354
+ ) &&
2355
+ ids.has(
2356
+ target
2357
+ )
2358
+ );
2359
+ }
2360
+ );
2361
+
2362
+ if (
2363
+ state.edge
2364
+ ) {
2365
+ links =
2366
+ links.filter(
2367
+ link =>
2368
+ link.type ===
2369
+ state.edge
2370
+ );
2371
+ }
2372
+
2373
+ return {
2374
+ nodes:
2375
+ nodes.map(
2376
+ node => ({
2377
+ ...node
2378
+ })
2379
+ ),
2380
+
2381
+ links:
2382
+ links.map(
2383
+ link => ({
2384
+ ...link,
2385
+
2386
+ source:
2387
+ typeof link.source ===
2388
+ "object"
2389
+ ? link.source.id
2390
+ : link.source,
2391
+
2392
+ target:
2393
+ typeof link.target ===
2394
+ "object"
2395
+ ? link.target.id
2396
+ : link.target,
2397
+ })
2398
+ ),
2399
+ };
2400
+ }
2401
+
2402
+ function renderStats(
2403
+ data
2404
+ ) {
2405
+ const dashboard =
2406
+ state.raw.dashboard ||
2407
+ {};
2408
+
2409
+ statsEl.innerHTML = `
2410
+ <div>
2411
+ <strong>
2412
+ ${data.nodes.length}
2413
+ </strong>
2414
+ nodes
2415
+ </div>
2416
+
2417
+ <div>
2418
+ <strong>
2419
+ ${data.links.length}
2420
+ </strong>
2421
+ relationships
2422
+ </div>
2423
+
2424
+ <div>
2425
+ <strong>
2426
+ ${state.raw.summary.files}
2427
+ </strong>
2428
+ files
2429
+ </div>
2430
+
2431
+ <div>
2432
+ <strong>
2433
+ ${state.raw.summary.clusters}
2434
+ </strong>
2435
+ subsystems
2436
+ </div>
2437
+
2438
+ <div class="status">
2439
+ ${escapeHtml(
2440
+ dashboard.name ||
2441
+ state.project
2442
+ )}
2443
+
2444
+ ${
2445
+ dashboard.hasArchitecture
2446
+ ? " · architecture"
2447
+ : ""
2448
+ }
2449
+
2450
+ ${
2451
+ dashboard.hasAnalysis
2452
+ ? " · analysis"
2453
+ : ""
2454
+ }
2455
+ </div>
2456
+ `;
2457
+ }
2458
+
2459
+ function applyFilters() {
2460
+ const data =
2461
+ filteredData();
2462
+
2463
+ state.graph.graphData(
2464
+ data
2465
+ );
2466
+
2467
+ renderStats(
2468
+ data
2469
+ );
2470
+ }
2471
+
2472
+ async function loadProject(
2473
+ project,
2474
+ options = {}
2475
+ ) {
2476
+ closeDetails();
2477
+
2478
+ const mode =
2479
+ options.mode ||
2480
+ "overview";
2481
+
2482
+ let loading =
2483
+ document.getElementById(
2484
+ "loading"
2485
+ );
2486
+
2487
+ if (!loading) {
2488
+ loading =
2489
+ document.createElement(
2490
+ "div"
2491
+ );
2492
+
2493
+ loading.id =
2494
+ "loading";
2495
+
2496
+ document.body
2497
+ .appendChild(
2498
+ loading
2499
+ );
2500
+ }
2501
+
2502
+ loading.textContent =
2503
+ `Loading ${project} · ${mode}…`;
2504
+
2505
+ const params =
2506
+ new URLSearchParams();
2507
+
2508
+ params.set(
2509
+ "project",
2510
+ project
2511
+ );
2512
+
2513
+ params.set(
2514
+ "mode",
2515
+ mode
2516
+ );
2517
+
2518
+ if (
2519
+ options.cluster
2520
+ ) {
2521
+ params.set(
2522
+ "cluster",
2523
+ options.cluster
2524
+ );
2525
+ }
2526
+
2527
+ if (
2528
+ options.file
2529
+ ) {
2530
+ params.set(
2531
+ "file",
2532
+ options.file
2533
+ );
2534
+ }
2535
+
2536
+ const response =
2537
+ await fetch(
2538
+ `/api/graph?${params.toString()}`
2539
+ );
2540
+
2541
+ if (
2542
+ !response.ok
2543
+ ) {
2544
+ const error =
2545
+ await response
2546
+ .json()
2547
+ .catch(
2548
+ () => null
2549
+ );
2550
+
2551
+ throw new Error(
2552
+ error?.error ||
2553
+ "Unable to load graph"
2554
+ );
2555
+ }
2556
+
2557
+ state.project =
2558
+ project;
2559
+
2560
+ state.viewMode =
2561
+ mode;
2562
+
2563
+ state.viewCluster =
2564
+ options.cluster ||
2565
+ "";
2566
+
2567
+ state.viewFile =
2568
+ options.file ||
2569
+ "";
2570
+
2571
+ state.raw =
2572
+ await response.json();
2573
+
2574
+ state.search =
2575
+ "";
2576
+
2577
+ state.cluster =
2578
+ "";
2579
+
2580
+ state.edge =
2581
+ "";
2582
+
2583
+ state.pathStart =
2584
+ null;
2585
+
2586
+ searchEl.value =
2587
+ "";
2588
+
2589
+ clusterEl.value =
2590
+ "";
2591
+
2592
+ edgeEl.value =
2593
+ "";
2594
+
2595
+ populateFilters();
2596
+
2597
+ const data =
2598
+ filteredData();
2599
+
2600
+ if (
2601
+ !state.graph
2602
+ ) {
2603
+ state.graph =
2604
+ ForceGraph3D()(
2605
+ graphEl
2606
+ )
2607
+ .backgroundColor(
2608
+ "#050608"
2609
+ )
2610
+
2611
+ .nodeLabel(
2612
+ node =>
2613
+ `${
2614
+ node.qualifiedName ||
2615
+ node.name
2616
+ }\n${
2617
+ node.filePath ||
2618
+ ""
2619
+ }`
2620
+ )
2621
+
2622
+ .nodeColor(
2623
+ node =>
2624
+ nodeColor(
2625
+ node
2626
+ )
2627
+ )
2628
+
2629
+ .nodeVal(
2630
+ node =>
2631
+ 2 +
2632
+ Math.min(
2633
+ 10,
2634
+ Math.log2(
2635
+ 1 +
2636
+ (
2637
+ node.incoming ||
2638
+ 0
2639
+ ) +
2640
+ (
2641
+ node.outgoing ||
2642
+ 0
2643
+ )
2644
+ )
2645
+ )
2646
+ )
2647
+
2648
+ .linkColor(
2649
+ link =>
2650
+ linkColor(
2651
+ link
2652
+ )
2653
+ )
2654
+
2655
+ .linkOpacity(
2656
+ .38
2657
+ )
2658
+
2659
+ .linkWidth(
2660
+ link =>
2661
+ link.type ===
2662
+ "CALL_REFERENCE"
2663
+ ? 1
2664
+ : .4
2665
+ )
2666
+
2667
+ /*
2668
+ * Particles across 200k relationships are extremely
2669
+ * expensive on mobile GPUs. Keep them disabled.
2670
+ */
2671
+ .linkDirectionalParticles(
2672
+ 0
2673
+ )
2674
+
2675
+ .onBackgroundClick(
2676
+ () => {
2677
+ closeDetails();
2678
+ }
2679
+ )
2680
+
2681
+ .onNodeClick(
2682
+ node => {
2683
+ if (
2684
+ node.viewKind ===
2685
+ "cluster" ||
2686
+ node.viewKind ===
2687
+ "file"
2688
+ ) {
2689
+ showDetails(
2690
+ node
2691
+ );
2692
+
2693
+ return;
2694
+ }
2695
+
2696
+ showDetails(
2697
+ node
2698
+ );
2699
+
2700
+ flyToNode(
2701
+ node
2702
+ );
2703
+ }
2704
+ );
2705
+ }
2706
+
2707
+ state.graph
2708
+ .cooldownTicks(
2709
+ mode ===
2710
+ "overview"
2711
+ ? 50
2712
+ : 80
2713
+ );
2714
+
2715
+ state.graph.graphData(
2716
+ data
2717
+ );
2718
+
2719
+ renderStats(
2720
+ data
2721
+ );
2722
+
2723
+ document.title =
2724
+ `${
2725
+ state.raw.dashboard
2726
+ ?.name ||
2727
+ project
2728
+ } · ToolNet Graph`;
2729
+
2730
+ loading =
2731
+ document.getElementById(
2732
+ "loading"
2733
+ );
2734
+
2735
+ if (loading) {
2736
+ loading.remove();
2737
+ }
2738
+
2739
+ setTimeout(
2740
+ () => {
2741
+ state.graph
2742
+ .zoomToFit(
2743
+ 500,
2744
+ 45
2745
+ );
2746
+ },
2747
+ 150
2748
+ );
2749
+ }
2750
+
2751
+ async function main() {
2752
+ const response =
2753
+ await fetch(
2754
+ "/api/projects"
2755
+ );
2756
+
2757
+ if (
2758
+ !response.ok
2759
+ ) {
2760
+ throw new Error(
2761
+ "Unable to load ToolNet projects"
2762
+ );
2763
+ }
2764
+
2765
+ const catalog =
2766
+ await response.json();
2767
+
2768
+ projectEl.innerHTML =
2769
+ "";
2770
+
2771
+ for (
2772
+ const project
2773
+ of catalog.projects
2774
+ ) {
2775
+ const option =
2776
+ document.createElement(
2777
+ "option"
2778
+ );
2779
+
2780
+ option.value =
2781
+ project.remote;
2782
+
2783
+ option.textContent =
2784
+ project.hasGraph
2785
+ ? project.name
2786
+ : `${project.name} · not indexed`;
2787
+
2788
+ option.disabled =
2789
+ !project.hasGraph;
2790
+
2791
+ projectEl.appendChild(
2792
+ option
2793
+ );
2794
+ }
2795
+
2796
+ const requested =
2797
+ new URLSearchParams(
2798
+ window.location.search
2799
+ ).get(
2800
+ "project"
2801
+ );
2802
+
2803
+ const requestedValid =
2804
+ catalog.projects
2805
+ .some(
2806
+ project =>
2807
+ project.remote ===
2808
+ requested &&
2809
+ project.hasGraph
2810
+ );
2811
+
2812
+ const selected =
2813
+ requestedValid
2814
+ ? requested
2815
+ : catalog.defaultProject;
2816
+
2817
+ if (!selected) {
2818
+ throw new Error(
2819
+ "No indexed projects found"
2820
+ );
2821
+ }
2822
+
2823
+ projectEl.value =
2824
+ selected;
2825
+
2826
+ await loadProject(
2827
+ selected
2828
+ );
2829
+ }
2830
+
2831
+ projectEl.addEventListener(
2832
+ "change",
2833
+ async () => {
2834
+ const project =
2835
+ projectEl.value;
2836
+
2837
+ if (!project) {
2838
+ return;
2839
+ }
2840
+
2841
+ const url =
2842
+ new URL(
2843
+ window.location.href
2844
+ );
2845
+
2846
+ url.searchParams.set(
2847
+ "project",
2848
+ project
2849
+ );
2850
+
2851
+ history.replaceState(
2852
+ {},
2853
+ "",
2854
+ url
2855
+ );
2856
+
2857
+ try {
2858
+ await loadProject(
2859
+ project
2860
+ );
2861
+ } catch (
2862
+ error
2863
+ ) {
2864
+ alert(
2865
+ error.message
2866
+ );
2867
+ }
2868
+ }
2869
+ );
2870
+
2871
+ searchEl.addEventListener(
2872
+ "input",
2873
+ () => {
2874
+ state.search =
2875
+ searchEl.value;
2876
+
2877
+ applyFilters();
2878
+ }
2879
+ );
2880
+
2881
+ clusterEl.addEventListener(
2882
+ "change",
2883
+ () => {
2884
+ state.cluster =
2885
+ clusterEl.value;
2886
+
2887
+ applyFilters();
2888
+ }
2889
+ );
2890
+
2891
+ edgeEl.addEventListener(
2892
+ "change",
2893
+ () => {
2894
+ state.edge =
2895
+ edgeEl.value;
2896
+
2897
+ applyFilters();
2898
+ }
2899
+ );
2900
+
2901
+ document
2902
+ .getElementById(
2903
+ "reset"
2904
+ )
2905
+ .addEventListener(
2906
+ "click",
2907
+ () => {
2908
+ if (
2909
+ state.viewMode !==
2910
+ "overview"
2911
+ ) {
2912
+ void loadProject(
2913
+ state.project
2914
+ );
2915
+
2916
+ return;
2917
+ }
2918
+
2919
+ searchEl.value =
2920
+ "";
2921
+
2922
+ clusterEl.value =
2923
+ "";
2924
+
2925
+ edgeEl.value =
2926
+ "";
2927
+
2928
+ state.search =
2929
+ "";
2930
+
2931
+ state.cluster =
2932
+ "";
2933
+
2934
+ state.edge =
2935
+ "";
2936
+
2937
+ state.pathStart =
2938
+ null;
2939
+
2940
+ resetPathHighlight();
2941
+
2942
+ closeDetails();
2943
+
2944
+ applyFilters();
2945
+
2946
+ state.graph
2947
+ .zoomToFit(
2948
+ 700,
2949
+ 60
2950
+ );
2951
+ }
2952
+ );
2953
+
2954
+ document.addEventListener(
2955
+ "keydown",
2956
+ event => {
2957
+ if (
2958
+ event.key ===
2959
+ "Escape"
2960
+ ) {
2961
+ closeDetails();
2962
+ }
2963
+ }
2964
+ );
2965
+
2966
+ main().catch(
2967
+ error => {
2968
+ const loading =
2969
+ document.getElementById(
2970
+ "loading"
2971
+ );
2972
+
2973
+ if (
2974
+ loading
2975
+ ) {
2976
+ loading.textContent =
2977
+ error.message;
2978
+ }
2979
+
2980
+ console.error(
2981
+ error
2982
+ );
2983
+ }
2984
+ );
2985
+ </script>
2986
+ </body>
2987
+ </html>