toolnet-memory 0.3.17 → 0.3.18

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