toolnet-memory 0.3.16 → 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.
Files changed (45) hide show
  1. package/.env.example +74 -0
  2. package/.release-target +1 -1
  3. package/README.md +157 -7
  4. package/bin/toolnet-memory +12 -0
  5. package/bundle/agy-hook.js +59 -59
  6. package/bundle/agy.js +58 -58
  7. package/bundle/api.js +15 -15
  8. package/bundle/audit.js +2 -0
  9. package/bundle/background-refresh.js +7 -7
  10. package/bundle/code-capabilities.js +2 -0
  11. package/bundle/codex.js +63 -63
  12. package/bundle/context-runtime.js +41 -41
  13. package/bundle/context.js +15 -15
  14. package/bundle/copilot-hook.js +73 -73
  15. package/bundle/cursor-hook.js +73 -73
  16. package/bundle/docker-healthcheck.js +3 -0
  17. package/bundle/doctor.js +4 -4
  18. package/bundle/full-index.js +20 -19
  19. package/bundle/gc.js +5 -3
  20. package/bundle/graph-cli.js +1 -1
  21. package/bundle/graph-index.js +5 -4
  22. package/bundle/graph-ui.js +4 -3
  23. package/bundle/grok-hook.js +52 -52
  24. package/bundle/handoff-refresh.js +2 -2
  25. package/bundle/help-cli.js +4 -4
  26. package/bundle/impact.js +5 -4
  27. package/bundle/incremental.js +5 -5
  28. package/bundle/init.js +35 -35
  29. package/bundle/kiro-hook.js +74 -74
  30. package/bundle/learner.js +8 -6
  31. package/bundle/mcp.js +19 -17
  32. package/bundle/memory-agent.js +4 -4
  33. package/bundle/memory-query.js +3 -3
  34. package/bundle/opencode.js +73 -73
  35. package/bundle/project-manual.js +7 -7
  36. package/bundle/public/index.html +1089 -2306
  37. package/bundle/runtime.js +16 -13
  38. package/bundle/semantic.js +6 -5
  39. package/bundle/service.js +11 -6
  40. package/bundle/snapshot.js +6 -4
  41. package/bundle/status-cli.js +4 -4
  42. package/bundle/toolnet-cli.js +49 -49
  43. package/bundle/work-continuity.js +7 -7
  44. package/package.json +26 -2
  45. package/release-manifest.json +57 -1
@@ -1,1500 +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
+ }
57
+
58
+ #toolbar {
59
+ top: max(14px, env(safe-area-inset-top));
123
60
 
124
- font-weight:
125
- 500;
126
- }
61
+ left: 14px;
127
62
 
128
- input,
129
- select,
130
- button {
131
- height:
132
- 42px;
63
+ right: 14px;
133
64
 
134
- min-width:
135
- 0;
65
+ padding: 14px;
136
66
 
137
- border:
138
- 1px solid
139
- rgba(255,255,255,.12);
67
+ display: grid;
140
68
 
141
- border-radius:
142
- 11px;
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;
143
75
 
144
- background:
145
- rgba(255,255,255,.06);
76
+ gap: 10px;
146
77
 
147
- color:
148
- white;
78
+ align-items: center;
79
+ }
149
80
 
150
- padding:
151
- 0 12px;
81
+ .brand {
82
+ padding: 0 8px;
152
83
 
153
- outline:
154
- none;
84
+ white-space: nowrap;
155
85
 
156
- font:
157
- inherit;
158
- }
86
+ font-weight: 750;
159
87
 
160
- button {
161
- cursor:
162
- pointer;
163
- }
88
+ font-size: 18px;
89
+ }
164
90
 
165
- #project {
166
- font-weight:
167
- 650;
168
- }
91
+ .brand span {
92
+ opacity: 0.45;
169
93
 
170
- #stats {
171
- left:
172
- 14px;
94
+ font-weight: 500;
95
+ }
173
96
 
174
- bottom:
175
- max(
176
- 14px,
177
- env(safe-area-inset-bottom)
178
- );
97
+ input,
98
+ select,
99
+ button {
100
+ height: 42px;
179
101
 
180
- min-width:
181
- 220px;
102
+ min-width: 0;
182
103
 
183
- padding:
184
- 14px 16px;
104
+ border: 1px solid rgba(255, 255, 255, 0.12);
185
105
 
186
- font-size:
187
- 13px;
106
+ border-radius: 11px;
188
107
 
189
- line-height:
190
- 1.7;
191
- }
108
+ background: rgba(255, 255, 255, 0.06);
192
109
 
193
- .status {
194
- margin-top:
195
- 5px;
110
+ color: white;
196
111
 
197
- opacity:
198
- .55;
112
+ padding: 0 12px;
199
113
 
200
- font-size:
201
- 11px;
202
- }
114
+ outline: none;
203
115
 
204
- #details {
205
- top:
206
- 96px;
116
+ font: inherit;
117
+ }
207
118
 
208
- right:
209
- 14px;
119
+ button {
120
+ cursor: pointer;
121
+ }
210
122
 
211
- bottom:
212
- 14px;
123
+ #project {
124
+ font-weight: 650;
125
+ }
213
126
 
214
- width:
215
- min(
216
- 410px,
217
- calc(100vw - 28px)
218
- );
127
+ #stats {
128
+ left: 14px;
219
129
 
220
- padding:
221
- 68px 20px 20px;
130
+ bottom: max(14px, env(safe-area-inset-bottom));
222
131
 
223
- overflow-y:
224
- auto;
132
+ min-width: 220px;
225
133
 
226
- transform:
227
- translateX(
228
- calc(100% + 30px)
229
- );
134
+ padding: 14px 16px;
230
135
 
231
- transition:
232
- transform .2s ease;
233
- }
136
+ font-size: 13px;
234
137
 
235
- #details.open {
236
- transform:
237
- translateX(0);
238
- }
138
+ line-height: 1.7;
139
+ }
239
140
 
240
- .details-close {
241
- position:
242
- absolute;
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;
438
271
 
439
- right:
440
- 10px;
272
+ place-items: center;
441
273
 
442
- bottom:
443
- 10px;
444
- }
274
+ background: #050608;
445
275
 
446
- #stats {
447
- left:
448
- 10px;
276
+ padding: 24px;
449
277
 
450
- bottom:
451
- max(
452
- 10px,
453
- env(safe-area-inset-bottom)
454
- );
278
+ text-align: center;
455
279
  }
456
- }
457
280
 
458
- @media (
459
- max-width: 1000px
460
- ) {
461
- #toolbar {
462
- grid-template-columns:
463
- repeat(12, minmax(0, 1fr));
281
+ @media (max-width: 1000px) {
282
+ #toolbar {
283
+ grid-template-columns: 1fr 1fr;
284
+
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
+ }
464
315
  }
465
316
 
466
- .brand {
467
- grid-column:
468
- 1 / -1;
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
+ }
469
359
  }
470
360
 
471
- #project {
472
- grid-column:
473
- 1 / 10;
361
+ .intel-summary {
362
+ display: grid;
474
363
 
475
- width:
476
- 100%;
364
+ grid-template-columns: 1fr 1fr;
365
+
366
+ gap: 8px;
367
+
368
+ margin-top: 18px;
477
369
  }
478
370
 
479
- #reset {
480
- grid-column:
481
- 10 / 13;
371
+ .intel-card {
372
+ padding: 12px;
482
373
 
483
- width:
484
- 100%;
374
+ border: 1px solid rgba(255, 255, 255, 0.08);
485
375
 
486
- padding-left:
487
- 8px;
376
+ border-radius: 12px;
488
377
 
489
- padding-right:
490
- 8px;
378
+ background: rgba(255, 255, 255, 0.035);
491
379
  }
492
380
 
493
- #search {
494
- grid-column:
495
- 1 / -1;
381
+ .intel-card strong {
382
+ display: block;
383
+
384
+ font-size: 18px;
496
385
 
497
- width:
498
- 100%;
386
+ margin-top: 3px;
499
387
  }
500
388
 
501
- #cluster {
502
- grid-column:
503
- 1 / 7;
389
+ .intel-card span {
390
+ opacity: 0.55;
504
391
 
505
- width:
506
- 100%;
392
+ font-size: 11px;
507
393
  }
508
394
 
509
- #edge {
510
- grid-column:
511
- 7 / 13;
395
+ .relation-list {
396
+ display: flex;
512
397
 
513
- width:
514
- 100%;
398
+ flex-direction: column;
399
+
400
+ gap: 6px;
515
401
  }
516
- }
517
402
 
403
+ .relation-item {
404
+ width: 100%;
518
405
 
519
- .intel-summary {
520
- display:
521
- grid;
406
+ height: auto;
522
407
 
523
- grid-template-columns:
524
- 1fr 1fr;
408
+ min-height: 44px;
525
409
 
526
- gap:
527
- 8px;
410
+ padding: 9px 11px;
528
411
 
529
- margin-top:
530
- 18px;
531
- }
412
+ display: block;
532
413
 
533
- .intel-card {
534
- padding:
535
- 12px;
414
+ text-align: left;
536
415
 
537
- border:
538
- 1px solid
539
- rgba(255,255,255,.08);
416
+ border-radius: 10px;
540
417
 
541
- border-radius:
542
- 12px;
418
+ cursor: pointer;
419
+ }
543
420
 
544
- background:
545
- rgba(255,255,255,.035);
546
- }
421
+ .relation-name {
422
+ display: block;
547
423
 
548
- .intel-card strong {
549
- display:
550
- block;
424
+ font-size: 12px;
551
425
 
552
- font-size:
553
- 18px;
426
+ font-weight: 650;
554
427
 
555
- margin-top:
556
- 3px;
557
- }
428
+ overflow-wrap: anywhere;
429
+ }
558
430
 
559
- .intel-card span {
560
- opacity:
561
- .55;
431
+ .relation-meta {
432
+ display: block;
562
433
 
563
- font-size:
564
- 11px;
565
- }
434
+ margin-top: 4px;
566
435
 
567
- .relation-list {
568
- display:
569
- flex;
436
+ opacity: 0.45;
570
437
 
571
- flex-direction:
572
- column;
438
+ font-size: 10px;
573
439
 
574
- gap:
575
- 6px;
576
- }
440
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
577
441
 
578
- .relation-item {
579
- width:
580
- 100%;
442
+ overflow-wrap: anywhere;
443
+ }
581
444
 
582
- height:
583
- auto;
445
+ .relation-more {
446
+ margin-top: 7px;
584
447
 
585
- min-height:
586
- 44px;
448
+ opacity: 0.45;
587
449
 
588
- padding:
589
- 9px 11px;
450
+ font-size: 11px;
451
+ }
590
452
 
591
- display:
592
- block;
453
+ .risk {
454
+ display: inline-flex;
593
455
 
594
- text-align:
595
- left;
456
+ padding: 5px 9px;
596
457
 
597
- border-radius:
598
- 10px;
458
+ margin-top: 8px;
599
459
 
600
- cursor:
601
- pointer;
602
- }
460
+ border: 1px solid rgba(255, 255, 255, 0.12);
603
461
 
604
- .relation-name {
605
- display:
606
- block;
462
+ border-radius: 999px;
607
463
 
608
- font-size:
609
- 12px;
464
+ font-size: 11px;
610
465
 
611
- font-weight:
612
- 650;
466
+ font-weight: 700;
467
+ }
613
468
 
614
- overflow-wrap:
615
- anywhere;
616
- }
469
+ .path-actions {
470
+ display: flex;
617
471
 
618
- .relation-meta {
619
- display:
620
- block;
472
+ gap: 8px;
621
473
 
622
- margin-top:
623
- 4px;
474
+ margin-top: 10px;
475
+ }
624
476
 
625
- opacity:
626
- .45;
477
+ .path-actions button {
478
+ flex: 1;
627
479
 
628
- font-size:
629
- 10px;
480
+ height: auto;
630
481
 
631
- font-family:
632
- ui-monospace,
633
- SFMono-Regular,
634
- Menlo,
635
- monospace;
482
+ min-height: 40px;
636
483
 
637
- overflow-wrap:
638
- anywhere;
639
- }
484
+ white-space: normal;
485
+ }
640
486
 
641
- .relation-more {
642
- margin-top:
643
- 7px;
487
+ .path-chain {
488
+ display: flex;
644
489
 
645
- opacity:
646
- .45;
490
+ flex-direction: column;
647
491
 
648
- font-size:
649
- 11px;
650
- }
492
+ gap: 5px;
651
493
 
652
- .risk {
653
- display:
654
- inline-flex;
494
+ margin-top: 9px;
495
+ }
655
496
 
656
- padding:
657
- 5px 9px;
497
+ .path-node {
498
+ height: auto;
658
499
 
659
- margin-top:
660
- 8px;
500
+ min-height: 36px;
661
501
 
662
- border:
663
- 1px solid
664
- rgba(255,255,255,.12);
502
+ padding: 7px 10px;
665
503
 
666
- border-radius:
667
- 999px;
504
+ text-align: left;
668
505
 
669
- font-size:
670
- 11px;
506
+ font-size: 11px;
507
+ }
508
+ </style>
509
+ </head>
671
510
 
672
- font-weight:
673
- 700;
674
- }
511
+ <body>
512
+ <div id="loading">Loading ToolNet projects…</div>
675
513
 
676
- .path-actions {
677
- display:
678
- flex;
514
+ <div id="graph"></div>
679
515
 
680
- gap:
681
- 8px;
516
+ <div id="toolbar" class="panel">
517
+ <div class="brand">
518
+ ToolNet Memory
519
+ <span>/ Graph</span>
520
+ </div>
682
521
 
683
- margin-top:
684
- 10px;
685
- }
522
+ <select id="project" aria-label="Project">
523
+ <option>Loading…</option>
524
+ </select>
686
525
 
687
- .path-actions button {
688
- flex:
689
- 1;
526
+ <button id="reset">Reset</button>
690
527
 
691
- height:
692
- auto;
528
+ <input id="search" placeholder="Search symbol or file…" />
693
529
 
694
- min-height:
695
- 40px;
530
+ <select id="cluster" aria-label="Subsystem">
531
+ <option value="">All subsystems</option>
532
+ </select>
696
533
 
697
- white-space:
698
- normal;
699
- }
534
+ <select id="edge" aria-label="Relation">
535
+ <option value="">All relations</option>
536
+ </select>
537
+ </div>
700
538
 
701
- .path-chain {
702
- display:
703
- flex;
539
+ <div id="stats" class="panel"></div>
704
540
 
705
- flex-direction:
706
- column;
541
+ <div id="details" class="panel"></div>
707
542
 
708
- gap:
709
- 5px;
543
+ <script src="/vendor/3d-force-graph.min.js"></script>
710
544
 
711
- margin-top:
712
- 9px;
713
- }
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,
714
597
 
715
- .path-node {
716
- height:
717
- auto;
598
+ graph: null,
718
599
 
719
- min-height:
720
- 36px;
600
+ project: '',
721
601
 
722
- padding:
723
- 7px 10px;
602
+ search: '',
724
603
 
725
- text-align:
726
- left;
604
+ cluster: '',
727
605
 
728
- font-size:
729
- 11px;
730
- }
606
+ edge: '',
731
607
 
732
- </style>
733
- </head>
608
+ selected: null,
734
609
 
735
- <body>
736
- <div id="loading">
737
- Loading ToolNet projects…
738
- </div>
610
+ pathStart: null,
739
611
 
740
- <div id="graph"></div>
612
+ highlightedPath: [],
741
613
 
742
- <div
743
- id="toolbar"
744
- class="panel"
745
- >
746
- <div class="brand">
747
- ToolNet Memory
748
- <span>/ Graph</span>
749
- </div>
614
+ viewMode: 'overview',
750
615
 
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
- );
616
+ viewCluster: '',
939
617
 
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
618
+ viewFile: '',
619
+ };
620
+
621
+ const graphEl = document.getElementById('graph');
622
+
623
+ const projectEl = document.getElementById('project');
624
+
625
+ const searchEl = document.getElementById('search');
626
+
627
+ const clusterEl = document.getElementById('cluster');
628
+
629
+ const edgeEl = document.getElementById('edge');
630
+
631
+ const statsEl = document.getElementById('stats');
632
+
633
+ const detailsEl = document.getElementById('details');
634
+
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',
964
648
  ];
965
- }
966
649
 
967
- function linkColor(
968
- link
969
- ) {
970
- const map = {
971
- CALL_REFERENCE:
972
- "rgba(255,190,90,.72)",
650
+ function escapeHtml(value) {
651
+ return String(value ?? '')
652
+ .replaceAll('&', '&amp;')
653
+ .replaceAll('<', '&lt;')
654
+ .replaceAll('>', '&gt;')
655
+ .replaceAll('"', '&quot;')
656
+ .replaceAll("'", '&#039;');
657
+ }
973
658
 
974
- CALLS:
975
- "rgba(255,255,255,.30)",
659
+ function hashCode(value) {
660
+ let hash = 0;
976
661
 
977
- IMPORTS:
978
- "rgba(100,170,255,.46)",
662
+ for (let i = 0; i < value.length; i++) {
663
+ hash = (hash << 5) - hash + value.charCodeAt(i);
979
664
 
980
- USES_TYPE:
981
- "rgba(190,130,255,.44)",
665
+ hash |= 0;
666
+ }
982
667
 
983
- WRITES:
984
- "rgba(255,100,100,.60)",
668
+ return Math.abs(hash);
669
+ }
985
670
 
986
- TESTS:
987
- "rgba(100,220,150,.42)",
671
+ function nodeColor(node) {
672
+ const value = node.clusterLabel || node.cluster || node.layer || node.type || 'other';
988
673
 
989
- IMPLEMENTS:
990
- "rgba(100,220,220,.48)",
674
+ return colors[hashCode(value) % colors.length];
675
+ }
991
676
 
992
- INHERITS:
993
- "rgba(235,140,255,.50)",
994
- };
677
+ function linkColor(link) {
678
+ const map = {
679
+ CALL_REFERENCE: 'rgba(255,190,90,.72)',
995
680
 
996
- return (
997
- map[link.type] ||
998
- "rgba(255,255,255,.18)"
999
- );
1000
- }
681
+ CALLS: 'rgba(255,255,255,.30)',
1001
682
 
1002
- function closeDetails() {
1003
- state.selected =
1004
- null;
683
+ IMPORTS: 'rgba(100,170,255,.46)',
1005
684
 
1006
- detailsEl
1007
- .classList
1008
- .remove(
1009
- "open"
1010
- );
685
+ USES_TYPE: 'rgba(190,130,255,.44)',
1011
686
 
1012
- detailsEl.innerHTML =
1013
- "";
1014
- }
687
+ WRITES: 'rgba(255,100,100,.60)',
1015
688
 
1016
- const CALL_RELATIONS =
1017
- new Set([
1018
- "CALLS",
1019
- "CALL_REFERENCE",
1020
- ]);
689
+ TESTS: 'rgba(100,220,150,.42)',
690
+
691
+ IMPLEMENTS: 'rgba(100,220,220,.48)',
692
+
693
+ INHERITS: 'rgba(235,140,255,.50)',
694
+ };
695
+
696
+ return map[link.type] || 'rgba(255,255,255,.18)';
697
+ }
698
+
699
+ function closeDetails() {
700
+ state.selected = null;
1021
701
 
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",
702
+ detailsEl.classList.remove('open');
703
+
704
+ detailsEl.innerHTML = '';
705
+ }
706
+
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',
1032
718
  ]);
1033
719
 
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
- }
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;
1092
737
  }
1093
738
 
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;
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
+ }
1118
750
  }
1119
751
 
1120
- const source =
1121
- endpointId(
1122
- link,
1123
- "source"
1124
- );
752
+ return [...map.values()];
753
+ }
1125
754
 
1126
- const target =
1127
- endpointId(
1128
- link,
1129
- "target"
1130
- );
755
+ function relatedNodes(nodeId, direction, allowed) {
756
+ const result = [];
1131
757
 
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
- );
758
+ for (const link of state.raw.links) {
759
+ if (allowed && !allowed.has(link.type)) {
760
+ continue;
1147
761
  }
1148
- }
1149
762
 
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
- );
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
+ }
1165
781
  }
1166
782
  }
1167
- }
1168
783
 
1169
- return uniqueNodes(
1170
- result
1171
- );
1172
- }
784
+ return uniqueNodes(result);
785
+ }
1173
786
 
1174
- function blastRadius(
1175
- startId,
1176
- maxDepth = 3
1177
- ) {
1178
- const visited =
1179
- new Map();
787
+ function blastRadius(startId, maxDepth = 3) {
788
+ const visited = new Map();
1180
789
 
1181
- const queue = [
1182
- {
1183
- id:
1184
- startId,
790
+ const queue = [
791
+ {
792
+ id: startId,
1185
793
 
1186
- depth:
1187
- 0,
794
+ depth: 0,
1188
795
 
1189
- score:
1190
- 0,
1191
- }
1192
- ];
796
+ score: 0,
797
+ },
798
+ ];
1193
799
 
1194
- visited.set(
1195
- startId,
1196
- {
1197
- depth:
1198
- 0,
800
+ visited.set(startId, {
801
+ depth: 0,
1199
802
 
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
- }
803
+ score: 0,
804
+ });
1217
805
 
1218
- for (
1219
- const link
1220
- of state.raw.links
1221
- ) {
1222
- const weight =
1223
- IMPACT_WEIGHTS[
1224
- link.type
1225
- ];
806
+ while (queue.length) {
807
+ const current = queue.shift();
1226
808
 
1227
- if (!weight) {
809
+ if (current.depth >= maxDepth) {
1228
810
  continue;
1229
811
  }
1230
812
 
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
- }
813
+ for (const link of state.raw.links) {
814
+ const weight = IMPACT_WEIGHTS[link.type];
1254
815
 
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
- );
816
+ if (!weight) {
817
+ continue;
818
+ }
1289
819
 
1290
- queue.push({
1291
- id:
1292
- source,
820
+ const source = endpointId(link, 'source');
1293
821
 
1294
- depth:
1295
- nextDepth,
822
+ const target = endpointId(link, 'target');
1296
823
 
1297
- score:
1298
- nextScore,
1299
- });
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
+ }
1300
854
  }
1301
855
  }
1302
- }
1303
856
 
1304
- visited.delete(
1305
- startId
1306
- );
857
+ visited.delete(startId);
1307
858
 
1308
- const impacted =
1309
- [
1310
- ...visited.entries()
1311
- ]
1312
- .map(
1313
- ([id, info]) => ({
1314
- node:
1315
- rawNode(
1316
- id
1317
- ),
859
+ const impacted = [...visited.entries()]
860
+ .map(([id, info]) => ({
861
+ node: rawNode(id),
1318
862
 
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
- );
863
+ ...info,
864
+ }))
865
+ .filter((item) => item.node)
866
+ .sort((a, b) => b.score - a.score);
1342
867
 
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
- }
868
+ const score = impacted.reduce((total, item) => total + item.score, 0);
1386
869
 
1387
- const queue = [
1388
- {
1389
- id:
1390
- fromId,
870
+ let risk = 'LOW';
1391
871
 
1392
- path: [
1393
- fromId
1394
- ],
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';
1395
878
  }
1396
- ];
1397
879
 
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;
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];
1414
890
  }
1415
891
 
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
- }
892
+ const queue = [
893
+ {
894
+ id: fromId,
1428
895
 
1429
- if (
1430
- endpointId(
1431
- link,
1432
- "source"
1433
- ) !==
1434
- current.id
1435
- ) {
1436
- continue;
1437
- }
896
+ path: [fromId],
897
+ },
898
+ ];
1438
899
 
1439
- const next =
1440
- endpointId(
1441
- link,
1442
- "target"
1443
- );
900
+ const visited = new Set([fromId]);
1444
901
 
1445
- if (
1446
- visited.has(
1447
- next
1448
- )
1449
- ) {
902
+ while (queue.length) {
903
+ const current = queue.shift();
904
+
905
+ if (current.path.length > maxDepth) {
1450
906
  continue;
1451
907
  }
1452
908
 
1453
- const path = [
1454
- ...current.path,
1455
- next,
1456
- ];
909
+ for (const link of state.raw.links) {
910
+ if (!DEPENDENCY_RELATIONS.has(link.type)) {
911
+ continue;
912
+ }
1457
913
 
1458
- if (
1459
- next ===
1460
- toId
1461
- ) {
1462
- return path;
1463
- }
914
+ if (endpointId(link, 'source') !== current.id) {
915
+ continue;
916
+ }
1464
917
 
1465
- visited.add(
1466
- next
1467
- );
918
+ const next = endpointId(link, 'target');
919
+
920
+ if (visited.has(next)) {
921
+ continue;
922
+ }
1468
923
 
1469
- queue.push({
1470
- id:
1471
- next,
924
+ const path = [...current.path, next];
1472
925
 
1473
- path,
1474
- });
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
+ }
1475
938
  }
939
+
940
+ return null;
1476
941
  }
1477
942
 
1478
- return null;
1479
- }
943
+ function relationHtml(title, nodes, limit = 8) {
944
+ const visible = nodes.slice(0, limit);
1480
945
 
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 `
946
+ return `
1493
947
  <div class="section">
1494
948
  <div class="section-title">
1495
- ${escapeHtml(
1496
- title
1497
- )}
949
+ ${escapeHtml(title)}
1498
950
  · ${nodes.length}
1499
951
  </div>
1500
952
 
@@ -1502,33 +954,24 @@
1502
954
  visible.length
1503
955
  ? `
1504
956
  <div class="relation-list">
1505
- ${
1506
- visible
1507
- .map(
1508
- node => `
957
+ ${visible
958
+ .map(
959
+ (node) => `
1509
960
  <button
1510
961
  class="relation-item"
1511
- data-jump-node="${escapeHtml(
1512
- node.id
1513
- )}"
962
+ data-jump-node="${escapeHtml(node.id)}"
1514
963
  >
1515
964
  <span class="relation-name">
1516
- ${escapeHtml(
1517
- node.qualifiedName ||
1518
- node.name
1519
- )}
965
+ ${escapeHtml(node.qualifiedName || node.name)}
1520
966
  </span>
1521
967
 
1522
968
  <span class="relation-meta">
1523
- ${escapeHtml(
1524
- node.filePath
1525
- )}
969
+ ${escapeHtml(node.filePath)}
1526
970
  </span>
1527
971
  </button>
1528
972
  `
1529
- )
1530
- .join("")
1531
- }
973
+ )
974
+ .join('')}
1532
975
  </div>
1533
976
  `
1534
977
  : `
@@ -1539,331 +982,167 @@
1539
982
  }
1540
983
 
1541
984
  ${
1542
- nodes.length >
1543
- limit
985
+ nodes.length > limit
1544
986
  ? `
1545
987
  <div class="relation-more">
1546
- +${
1547
- nodes.length -
1548
- limit
1549
- } more
988
+ +${nodes.length - limit} more
1550
989
  </div>
1551
990
  `
1552
- : ""
991
+ : ''
1553
992
  }
1554
993
  </div>
1555
994
  `;
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
- );
995
+ }
1593
996
 
1594
- const target =
1595
- endpointId(
1596
- link,
1597
- "target"
1598
- );
997
+ function resetPathHighlight() {
998
+ state.highlightedPath = [];
1599
999
 
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;
1000
+ if (!state.graph) {
1001
+ return;
1613
1002
  }
1003
+
1004
+ state.graph
1005
+ .linkWidth((link) => (link.type === 'CALL_REFERENCE' ? 1.2 : 0.45))
1006
+ .linkColor((link) => linkColor(link));
1614
1007
  }
1615
1008
 
1616
- return false;
1617
- }
1009
+ function isPathLink(link, path) {
1010
+ const source = endpointId(link, 'source');
1011
+
1012
+ const target = endpointId(link, 'target');
1618
1013
 
1619
- function highlightPath(
1620
- path
1621
- ) {
1622
- state.highlightedPath =
1623
- path || [];
1014
+ for (let i = 0; i < path.length - 1; i++) {
1015
+ if (path[i] === source && path[i + 1] === target) {
1016
+ return true;
1017
+ }
1018
+ }
1624
1019
 
1625
- if (
1626
- !state.graph
1627
- ) {
1628
- return;
1020
+ return false;
1629
1021
  }
1630
1022
 
1631
- state.graph
1632
- .linkWidth(
1633
- link =>
1634
- isPathLink(
1635
- link,
1636
- state.highlightedPath
1637
- )
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)
1638
1033
  ? 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
- );
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
+ }
1042
+
1043
+ function flyToNode(node) {
1044
+ const distance = 70;
1671
1045
 
1672
- const ratio =
1673
- 1 +
1674
- distance /
1675
- length;
1046
+ const length = Math.hypot(node.x || 1, node.y || 1, node.z || 1);
1676
1047
 
1677
- state.graph
1678
- .cameraPosition(
1048
+ const ratio = 1 + distance / length;
1049
+
1050
+ state.graph.cameraPosition(
1679
1051
  {
1680
- x:
1681
- node.x *
1682
- ratio,
1052
+ x: node.x * ratio,
1683
1053
 
1684
- y:
1685
- node.y *
1686
- ratio,
1054
+ y: node.y * ratio,
1687
1055
 
1688
- z:
1689
- node.z *
1690
- ratio,
1056
+ z: node.z * ratio,
1691
1057
  },
1692
1058
 
1693
1059
  node,
1694
1060
 
1695
1061
  800
1696
1062
  );
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
- );
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);
1736
1085
 
1737
1086
  if (!node) {
1738
1087
  return;
1739
1088
  }
1740
1089
 
1741
- showDetails(
1742
- node
1743
- );
1090
+ showDetails(node);
1744
1091
 
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",
1092
+ flyToNode(node);
1093
+ }, 100);
1094
+ }
1765
1095
 
1766
- cluster:
1767
- node.clusterLabel ||
1768
- node.name,
1769
- }
1770
- );
1096
+ function showDetails(node) {
1097
+ if (node.viewKind === 'cluster') {
1098
+ void loadProject(state.project, {
1099
+ mode: 'files',
1771
1100
 
1772
- return;
1773
- }
1101
+ cluster: node.clusterLabel || node.name,
1102
+ });
1774
1103
 
1775
- if (
1776
- node.viewKind ===
1777
- "file"
1778
- ) {
1779
- void loadProject(
1780
- state.project,
1781
- {
1782
- mode:
1783
- "symbols",
1104
+ return;
1105
+ }
1784
1106
 
1785
- file:
1786
- node.filePath,
1787
- }
1788
- );
1107
+ if (node.viewKind === 'file') {
1108
+ void loadProject(state.project, {
1109
+ mode: 'symbols',
1789
1110
 
1790
- return;
1791
- }
1111
+ file: node.filePath,
1112
+ });
1113
+
1114
+ return;
1115
+ }
1792
1116
 
1793
- state.selected =
1794
- node;
1117
+ state.selected = node;
1795
1118
 
1796
- detailsEl
1797
- .classList
1798
- .add(
1799
- "open"
1800
- );
1119
+ detailsEl.classList.add('open');
1801
1120
 
1802
- const callers =
1803
- relatedNodes(
1804
- node.id,
1805
- "incoming",
1806
- CALL_RELATIONS
1807
- );
1121
+ const callers = relatedNodes(node.id, 'incoming', CALL_RELATIONS);
1808
1122
 
1809
- const callees =
1810
- relatedNodes(
1811
- node.id,
1812
- "outgoing",
1813
- CALL_RELATIONS
1814
- );
1123
+ const callees = relatedNodes(node.id, 'outgoing', CALL_RELATIONS);
1815
1124
 
1816
- const dependencies =
1817
- relatedNodes(
1818
- node.id,
1819
- "outgoing",
1820
- DEPENDENCY_RELATIONS
1821
- );
1125
+ const dependencies = relatedNodes(node.id, 'outgoing', DEPENDENCY_RELATIONS);
1822
1126
 
1823
- const dependents =
1824
- relatedNodes(
1825
- node.id,
1826
- "incoming",
1827
- DEPENDENCY_RELATIONS
1828
- );
1127
+ const dependents = relatedNodes(node.id, 'incoming', DEPENDENCY_RELATIONS);
1829
1128
 
1830
- const blast =
1831
- blastRadius(
1832
- node.id,
1833
- 3
1834
- );
1129
+ const blast = blastRadius(node.id, 3);
1835
1130
 
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
- );
1131
+ let path = null;
1849
1132
 
1850
- if (path) {
1851
- highlightPath(
1852
- path
1853
- );
1854
- } else {
1855
- resetPathHighlight();
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
+ }
1856
1141
  }
1857
- }
1858
1142
 
1859
- const pathStartNode =
1860
- state.pathStart
1861
- ? rawNode(
1862
- state.pathStart
1863
- )
1864
- : null;
1143
+ const pathStartNode = state.pathStart ? rawNode(state.pathStart) : null;
1865
1144
 
1866
- detailsEl.innerHTML = `
1145
+ detailsEl.innerHTML = `
1867
1146
  <button
1868
1147
  class="details-close"
1869
1148
  id="details-close"
@@ -1874,47 +1153,36 @@
1874
1153
  </button>
1875
1154
 
1876
1155
  <div class="detail-title">
1877
- ${escapeHtml(
1878
- node.qualifiedName ||
1879
- node.name
1880
- )}
1156
+ ${escapeHtml(node.qualifiedName || node.name)}
1881
1157
  </div>
1882
1158
 
1883
1159
  <div class="path">
1884
- ${escapeHtml(
1885
- node.filePath
1886
- )}
1160
+ ${escapeHtml(node.filePath)}
1887
1161
  </div>
1888
1162
 
1889
1163
  <div>
1890
1164
  <span class="tag">
1891
- ${escapeHtml(
1892
- node.type
1893
- )}
1165
+ ${escapeHtml(node.type)}
1894
1166
  </span>
1895
1167
 
1896
1168
  ${
1897
1169
  node.clusterLabel
1898
1170
  ? `
1899
1171
  <span class="tag">
1900
- ${escapeHtml(
1901
- node.clusterLabel
1902
- )}
1172
+ ${escapeHtml(node.clusterLabel)}
1903
1173
  </span>
1904
1174
  `
1905
- : ""
1175
+ : ''
1906
1176
  }
1907
1177
 
1908
1178
  ${
1909
1179
  node.layer
1910
1180
  ? `
1911
1181
  <span class="tag">
1912
- ${escapeHtml(
1913
- node.layer
1914
- )}
1182
+ ${escapeHtml(node.layer)}
1915
1183
  </span>
1916
1184
  `
1917
- : ""
1185
+ : ''
1918
1186
  }
1919
1187
 
1920
1188
  ${
@@ -1922,13 +1190,10 @@
1922
1190
  ? `
1923
1191
  <span class="tag">
1924
1192
  dead-code:
1925
- ${escapeHtml(
1926
- node.deadCode
1927
- .confidence
1928
- )}
1193
+ ${escapeHtml(node.deadCode.confidence)}
1929
1194
  </span>
1930
1195
  `
1931
- : ""
1196
+ : ''
1932
1197
  }
1933
1198
  </div>
1934
1199
 
@@ -1996,39 +1261,19 @@
1996
1261
  ${
1997
1262
  blast.impacted.length
1998
1263
  ? relationHtml(
1999
- "Top Impacted",
2000
- blast.impacted
2001
- .slice(
2002
- 0,
2003
- 8
2004
- )
2005
- .map(
2006
- item =>
2007
- item.node
2008
- )
1264
+ 'Top Impacted',
1265
+ blast.impacted.slice(0, 8).map((item) => item.node)
2009
1266
  )
2010
- : ""
1267
+ : ''
2011
1268
  }
2012
1269
 
2013
- ${relationHtml(
2014
- "Callers",
2015
- callers
2016
- )}
1270
+ ${relationHtml('Callers', callers)}
2017
1271
 
2018
- ${relationHtml(
2019
- "Callees",
2020
- callees
2021
- )}
1272
+ ${relationHtml('Callees', callees)}
2022
1273
 
2023
- ${relationHtml(
2024
- "Dependencies",
2025
- dependencies
2026
- )}
1274
+ ${relationHtml('Dependencies', dependencies)}
2027
1275
 
2028
- ${relationHtml(
2029
- "Dependents",
2030
- dependents
2031
- )}
1276
+ ${relationHtml('Dependents', dependents)}
2032
1277
 
2033
1278
  <div class="section">
2034
1279
  <div class="section-title">
@@ -2047,11 +1292,7 @@
2047
1292
  <div class="path">
2048
1293
  From:
2049
1294
  ${escapeHtml(
2050
- pathStartNode
2051
- ?.qualifiedName ||
2052
- pathStartNode
2053
- ?.name ||
2054
- state.pathStart
1295
+ pathStartNode?.qualifiedName || pathStartNode?.name || state.pathStart
2055
1296
  )}
2056
1297
  </div>
2057
1298
  `
@@ -2062,12 +1303,7 @@
2062
1303
  id="path-start"
2063
1304
  type="button"
2064
1305
  >
2065
- ${
2066
- state.pathStart ===
2067
- node.id
2068
- ? "Start selected"
2069
- : "Set path start"
2070
- }
1306
+ ${state.pathStart === node.id ? 'Start selected' : 'Set path start'}
2071
1307
  </button>
2072
1308
 
2073
1309
  ${
@@ -2080,333 +1316,159 @@
2080
1316
  Clear path
2081
1317
  </button>
2082
1318
  `
2083
- : ""
1319
+ : ''
2084
1320
  }
2085
1321
  </div>
2086
1322
 
2087
1323
  ${
2088
- state.pathStart &&
2089
- state.pathStart !==
2090
- node.id
2091
- ? (
2092
- path
2093
- ? `
1324
+ state.pathStart && state.pathStart !== node.id
1325
+ ? path
1326
+ ? `
2094
1327
  <div class="path-chain">
2095
- ${
2096
- path.map(
2097
- id => {
2098
- const item =
2099
- rawNode(
2100
- id
2101
- );
2102
-
2103
- return `
1328
+ ${path
1329
+ .map((id) => {
1330
+ const item = rawNode(id);
1331
+
1332
+ return `
2104
1333
  <button
2105
1334
  class="path-node"
2106
- data-jump-node="${escapeHtml(
2107
- id
2108
- )}"
1335
+ data-jump-node="${escapeHtml(id)}"
2109
1336
  >
2110
- ${escapeHtml(
2111
- item?.qualifiedName ||
2112
- item?.name ||
2113
- id
2114
- )}
1337
+ ${escapeHtml(item?.qualifiedName || item?.name || id)}
2115
1338
  </button>
2116
1339
  `;
2117
- }
2118
- ).join("")
2119
- }
1340
+ })
1341
+ .join('')}
2120
1342
  </div>
2121
1343
  `
2122
- : `
1344
+ : `
2123
1345
  <div class="relation-more">
2124
1346
  Không tìm thấy directed path
2125
1347
  trong 10 bước.
2126
1348
  </div>
2127
1349
  `
2128
- )
2129
- : ""
1350
+ : ''
2130
1351
  }
2131
1352
  </div>
2132
1353
  `;
2133
1354
 
2134
- document
2135
- .getElementById(
2136
- "details-close"
2137
- )
2138
- ?.addEventListener(
2139
- "click",
2140
- 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) => {
2141
1362
  event.stopPropagation();
2142
- closeDetails();
2143
- }
2144
- );
2145
1363
 
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
- );
1364
+ jumpToNode(element.dataset.jumpNode);
1365
+ });
1366
+ });
2166
1367
 
2167
- document
2168
- .getElementById(
2169
- "path-start"
2170
- )
2171
- ?.addEventListener(
2172
- "click",
2173
- event => {
2174
- event.stopPropagation();
1368
+ document.getElementById('path-start')?.addEventListener('click', (event) => {
1369
+ event.stopPropagation();
2175
1370
 
2176
- state.pathStart =
2177
- node.id;
1371
+ state.pathStart = node.id;
2178
1372
 
2179
- resetPathHighlight();
1373
+ resetPathHighlight();
2180
1374
 
2181
- showDetails(
2182
- node
2183
- );
2184
- }
2185
- );
1375
+ showDetails(node);
1376
+ });
2186
1377
 
2187
- document
2188
- .getElementById(
2189
- "path-clear"
2190
- )
2191
- ?.addEventListener(
2192
- "click",
2193
- event => {
2194
- event.stopPropagation();
1378
+ document.getElementById('path-clear')?.addEventListener('click', (event) => {
1379
+ event.stopPropagation();
2195
1380
 
2196
- state.pathStart =
2197
- null;
1381
+ state.pathStart = null;
2198
1382
 
2199
- resetPathHighlight();
1383
+ resetPathHighlight();
2200
1384
 
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
- );
1385
+ showDetails(node);
1386
+ });
1387
+ }
2236
1388
 
2237
- option.value =
2238
- value;
1389
+ function populateFilters() {
1390
+ clusterEl.innerHTML = '<option value="">All subsystems</option>';
2239
1391
 
2240
- option.textContent =
2241
- value;
1392
+ edgeEl.innerHTML = '<option value="">All relations</option>';
2242
1393
 
2243
- clusterEl.appendChild(
2244
- option
2245
- );
2246
- }
1394
+ const clusters = [
1395
+ ...new Set(state.raw.nodes.map((node) => node.clusterLabel).filter(Boolean)),
1396
+ ].sort();
2247
1397
 
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
- );
1398
+ for (const value of clusters) {
1399
+ const option = document.createElement('option');
2268
1400
 
2269
- option.value =
2270
- value;
1401
+ option.value = value;
2271
1402
 
2272
- option.textContent =
2273
- value;
1403
+ option.textContent = value;
2274
1404
 
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
- }
1405
+ clusterEl.appendChild(option);
1406
+ }
2300
1407
 
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
- );
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
+ }
2325
1419
  }
2326
1420
 
2327
- const ids =
2328
- new Set(
2329
- nodes.map(
2330
- node =>
2331
- node.id
2332
- )
2333
- );
1421
+ function filteredData() {
1422
+ const search = state.search.trim().toLowerCase();
2334
1423
 
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
- );
1424
+ let nodes = state.raw.nodes;
2361
1425
 
2362
- if (
2363
- state.edge
2364
- ) {
2365
- links =
2366
- links.filter(
2367
- link =>
2368
- link.type ===
2369
- state.edge
1426
+ if (state.cluster) {
1427
+ nodes = nodes.filter((node) => node.clusterLabel === state.cluster);
1428
+ }
1429
+
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)
2370
1436
  );
2371
- }
1437
+ }
2372
1438
 
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
- }
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;
1445
+
1446
+ return ids.has(source) && ids.has(target);
1447
+ });
1448
+
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
+ })),
2401
1457
 
2402
- function renderStats(
2403
- data
2404
- ) {
2405
- const dashboard =
2406
- state.raw.dashboard ||
2407
- {};
1458
+ links: links.map((link) => ({
1459
+ ...link,
2408
1460
 
2409
- statsEl.innerHTML = `
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 = `
2410
1472
  <div>
2411
1473
  <strong>
2412
1474
  ${data.nodes.length}
@@ -2436,552 +1498,273 @@
2436
1498
  </div>
2437
1499
 
2438
1500
  <div class="status">
2439
- ${escapeHtml(
2440
- dashboard.name ||
2441
- state.project
2442
- )}
1501
+ ${escapeHtml(dashboard.name || state.project)}
2443
1502
 
2444
- ${
2445
- dashboard.hasArchitecture
2446
- ? " · architecture"
2447
- : ""
2448
- }
1503
+ ${dashboard.hasArchitecture ? ' · architecture' : ''}
2449
1504
 
2450
- ${
2451
- dashboard.hasAnalysis
2452
- ? " · analysis"
2453
- : ""
2454
- }
1505
+ ${dashboard.hasAnalysis ? ' · analysis' : ''}
2455
1506
  </div>
2456
1507
  `;
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
- );
1508
+ }
2486
1509
 
2487
- if (!loading) {
2488
- loading =
2489
- document.createElement(
2490
- "div"
2491
- );
1510
+ function applyFilters() {
1511
+ const data = filteredData();
2492
1512
 
2493
- loading.id =
2494
- "loading";
1513
+ state.graph.graphData(data);
2495
1514
 
2496
- document.body
2497
- .appendChild(
2498
- loading
2499
- );
1515
+ renderStats(data);
2500
1516
  }
2501
1517
 
2502
- loading.textContent =
2503
- `Loading ${project} · ${mode}…`;
1518
+ async function loadProject(project, options = {}) {
1519
+ closeDetails();
2504
1520
 
2505
- const params =
2506
- new URLSearchParams();
1521
+ const mode = options.mode || 'overview';
2507
1522
 
2508
- params.set(
2509
- "project",
2510
- project
2511
- );
1523
+ let loading = document.getElementById('loading');
2512
1524
 
2513
- params.set(
2514
- "mode",
2515
- mode
2516
- );
1525
+ if (!loading) {
1526
+ loading = document.createElement('div');
2517
1527
 
2518
- if (
2519
- options.cluster
2520
- ) {
2521
- params.set(
2522
- "cluster",
2523
- options.cluster
2524
- );
2525
- }
1528
+ loading.id = 'loading';
2526
1529
 
2527
- if (
2528
- options.file
2529
- ) {
2530
- params.set(
2531
- "file",
2532
- options.file
2533
- );
2534
- }
1530
+ document.body.appendChild(loading);
1531
+ }
2535
1532
 
2536
- const response =
2537
- await fetch(
2538
- `/api/graph?${params.toString()}`
2539
- );
1533
+ loading.textContent = `Loading ${project} · ${mode}…`;
2540
1534
 
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
- }
1535
+ const params = new URLSearchParams();
2556
1536
 
2557
- state.project =
2558
- project;
1537
+ params.set('project', project);
2559
1538
 
2560
- state.viewMode =
2561
- mode;
1539
+ params.set('mode', mode);
2562
1540
 
2563
- state.viewCluster =
2564
- options.cluster ||
2565
- "";
1541
+ if (options.cluster) {
1542
+ params.set('cluster', options.cluster);
1543
+ }
2566
1544
 
2567
- state.viewFile =
2568
- options.file ||
2569
- "";
1545
+ if (options.file) {
1546
+ params.set('file', options.file);
1547
+ }
2570
1548
 
2571
- state.raw =
2572
- await response.json();
1549
+ const response = await graphApiFetch(`/api/graph?${params.toString()}`);
2573
1550
 
2574
- state.search =
2575
- "";
1551
+ if (!response.ok) {
1552
+ const error = await response.json().catch(() => null);
2576
1553
 
2577
- state.cluster =
2578
- "";
1554
+ throw new Error(error?.error || 'Unable to load graph');
1555
+ }
2579
1556
 
2580
- state.edge =
2581
- "";
1557
+ state.project = project;
2582
1558
 
2583
- state.pathStart =
2584
- null;
1559
+ state.viewMode = mode;
2585
1560
 
2586
- searchEl.value =
2587
- "";
1561
+ state.viewCluster = options.cluster || '';
2588
1562
 
2589
- clusterEl.value =
2590
- "";
1563
+ state.viewFile = options.file || '';
2591
1564
 
2592
- edgeEl.value =
2593
- "";
1565
+ state.raw = await response.json();
2594
1566
 
2595
- populateFilters();
1567
+ state.search = '';
2596
1568
 
2597
- const data =
2598
- filteredData();
1569
+ state.cluster = '';
2599
1570
 
2600
- if (
2601
- !state.graph
2602
- ) {
2603
- state.graph =
2604
- ForceGraph3D()(
2605
- graphEl
2606
- )
2607
- .backgroundColor(
2608
- "#050608"
2609
- )
1571
+ state.edge = '';
2610
1572
 
2611
- .nodeLabel(
2612
- node =>
2613
- `${
2614
- node.qualifiedName ||
2615
- node.name
2616
- }\n${
2617
- node.filePath ||
2618
- ""
2619
- }`
2620
- )
1573
+ state.pathStart = null;
2621
1574
 
2622
- .nodeColor(
2623
- node =>
2624
- nodeColor(
2625
- node
2626
- )
2627
- )
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))
2628
1592
 
2629
1593
  .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
- )
1594
+ (node) => 2 + Math.min(10, Math.log2(1 + (node.incoming || 0) + (node.outgoing || 0)))
2646
1595
  )
2647
1596
 
2648
- .linkColor(
2649
- link =>
2650
- linkColor(
2651
- link
2652
- )
2653
- )
1597
+ .linkColor((link) => linkColor(link))
2654
1598
 
2655
- .linkOpacity(
2656
- .38
2657
- )
1599
+ .linkOpacity(0.38)
2658
1600
 
2659
- .linkWidth(
2660
- link =>
2661
- link.type ===
2662
- "CALL_REFERENCE"
2663
- ? 1
2664
- : .4
2665
- )
1601
+ .linkWidth((link) => (link.type === 'CALL_REFERENCE' ? 1 : 0.4))
2666
1602
 
2667
1603
  /*
2668
1604
  * Particles across 200k relationships are extremely
2669
1605
  * expensive on mobile GPUs. Keep them disabled.
2670
1606
  */
2671
- .linkDirectionalParticles(
2672
- 0
2673
- )
1607
+ .linkDirectionalParticles(0)
2674
1608
 
2675
- .onBackgroundClick(
2676
- () => {
2677
- closeDetails();
2678
- }
2679
- )
1609
+ .onBackgroundClick(() => {
1610
+ closeDetails();
1611
+ })
2680
1612
 
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
- );
1613
+ .onNodeClick((node) => {
1614
+ if (node.viewKind === 'cluster' || node.viewKind === 'file') {
1615
+ showDetails(node);
1616
+
1617
+ return;
2703
1618
  }
2704
- );
2705
- }
2706
1619
 
2707
- state.graph
2708
- .cooldownTicks(
2709
- mode ===
2710
- "overview"
2711
- ? 50
2712
- : 80
2713
- );
1620
+ showDetails(node);
1621
+
1622
+ flyToNode(node);
1623
+ });
1624
+ }
2714
1625
 
2715
- state.graph.graphData(
2716
- data
2717
- );
1626
+ state.graph.cooldownTicks(mode === 'overview' ? 50 : 80);
2718
1627
 
2719
- renderStats(
2720
- data
2721
- );
1628
+ state.graph.graphData(data);
2722
1629
 
2723
- document.title =
2724
- `${
2725
- state.raw.dashboard
2726
- ?.name ||
2727
- project
2728
- } · ToolNet Graph`;
1630
+ renderStats(data);
2729
1631
 
2730
- loading =
2731
- document.getElementById(
2732
- "loading"
2733
- );
1632
+ document.title = `${state.raw.dashboard?.name || project} · ToolNet Graph`;
2734
1633
 
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
- );
1634
+ loading = document.getElementById('loading');
2756
1635
 
2757
- if (
2758
- !response.ok
2759
- ) {
2760
- throw new Error(
2761
- "Unable to load ToolNet projects"
2762
- );
1636
+ if (loading) {
1637
+ loading.remove();
1638
+ }
1639
+
1640
+ setTimeout(() => {
1641
+ state.graph.zoomToFit(500, 45);
1642
+ }, 150);
2763
1643
  }
2764
1644
 
2765
- const catalog =
2766
- await response.json();
1645
+ async function main() {
1646
+ const response = await graphApiFetch('/api/projects');
2767
1647
 
2768
- projectEl.innerHTML =
2769
- "";
1648
+ if (!response.ok) {
1649
+ throw new Error('Unable to load ToolNet projects');
1650
+ }
2770
1651
 
2771
- for (
2772
- const project
2773
- of catalog.projects
2774
- ) {
2775
- const option =
2776
- document.createElement(
2777
- "option"
2778
- );
1652
+ const catalog = await response.json();
2779
1653
 
2780
- option.value =
2781
- project.remote;
1654
+ projectEl.innerHTML = '';
2782
1655
 
2783
- option.textContent =
2784
- project.hasGraph
2785
- ? project.name
2786
- : `${project.name} · not indexed`;
1656
+ for (const project of catalog.projects) {
1657
+ const option = document.createElement('option');
2787
1658
 
2788
- option.disabled =
2789
- !project.hasGraph;
1659
+ option.value = project.remote;
2790
1660
 
2791
- projectEl.appendChild(
2792
- option
2793
- );
2794
- }
1661
+ option.textContent = project.hasGraph ? project.name : `${project.name} · not indexed`;
2795
1662
 
2796
- const requested =
2797
- new URLSearchParams(
2798
- window.location.search
2799
- ).get(
2800
- "project"
2801
- );
1663
+ option.disabled = !project.hasGraph;
2802
1664
 
2803
- const requestedValid =
2804
- catalog.projects
2805
- .some(
2806
- project =>
2807
- project.remote ===
2808
- requested &&
2809
- project.hasGraph
2810
- );
1665
+ projectEl.appendChild(option);
1666
+ }
2811
1667
 
2812
- const selected =
2813
- requestedValid
2814
- ? requested
2815
- : catalog.defaultProject;
1668
+ const requested = new URLSearchParams(window.location.search).get('project');
2816
1669
 
2817
- if (!selected) {
2818
- throw new Error(
2819
- "No indexed projects found"
1670
+ const requestedValid = catalog.projects.some(
1671
+ (project) => project.remote === requested && project.hasGraph
2820
1672
  );
2821
- }
2822
1673
 
2823
- projectEl.value =
2824
- selected;
1674
+ const selected = requestedValid ? requested : catalog.defaultProject;
2825
1675
 
2826
- await loadProject(
2827
- selected
2828
- );
2829
- }
1676
+ if (!selected) {
1677
+ throw new Error('No indexed projects found');
1678
+ }
1679
+
1680
+ projectEl.value = selected;
1681
+
1682
+ await loadProject(selected);
1683
+ }
2830
1684
 
2831
- projectEl.addEventListener(
2832
- "change",
2833
- async () => {
2834
- const project =
2835
- projectEl.value;
1685
+ projectEl.addEventListener('change', async () => {
1686
+ const project = projectEl.value;
2836
1687
 
2837
1688
  if (!project) {
2838
1689
  return;
2839
1690
  }
2840
1691
 
2841
- const url =
2842
- new URL(
2843
- window.location.href
2844
- );
1692
+ const url = new URL(window.location.href);
2845
1693
 
2846
- url.searchParams.set(
2847
- "project",
2848
- project
2849
- );
1694
+ url.searchParams.set('project', project);
2850
1695
 
2851
- history.replaceState(
2852
- {},
2853
- "",
2854
- url
2855
- );
1696
+ history.replaceState({}, '', url);
2856
1697
 
2857
1698
  try {
2858
- await loadProject(
2859
- project
2860
- );
2861
- } catch (
2862
- error
2863
- ) {
2864
- alert(
2865
- error.message
2866
- );
1699
+ await loadProject(project);
1700
+ } catch (error) {
1701
+ alert(error.message);
2867
1702
  }
2868
- }
2869
- );
1703
+ });
2870
1704
 
2871
- searchEl.addEventListener(
2872
- "input",
2873
- () => {
2874
- state.search =
2875
- searchEl.value;
1705
+ searchEl.addEventListener('input', () => {
1706
+ state.search = searchEl.value;
2876
1707
 
2877
1708
  applyFilters();
2878
- }
2879
- );
1709
+ });
2880
1710
 
2881
- clusterEl.addEventListener(
2882
- "change",
2883
- () => {
2884
- state.cluster =
2885
- clusterEl.value;
1711
+ clusterEl.addEventListener('change', () => {
1712
+ state.cluster = clusterEl.value;
2886
1713
 
2887
1714
  applyFilters();
2888
- }
2889
- );
1715
+ });
2890
1716
 
2891
- edgeEl.addEventListener(
2892
- "change",
2893
- () => {
2894
- state.edge =
2895
- edgeEl.value;
1717
+ edgeEl.addEventListener('change', () => {
1718
+ state.edge = edgeEl.value;
2896
1719
 
2897
1720
  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
- );
1721
+ });
2915
1722
 
2916
- return;
2917
- }
1723
+ document.getElementById('reset').addEventListener('click', () => {
1724
+ if (state.viewMode !== 'overview') {
1725
+ void loadProject(state.project);
2918
1726
 
2919
- searchEl.value =
2920
- "";
1727
+ return;
1728
+ }
2921
1729
 
2922
- clusterEl.value =
2923
- "";
1730
+ searchEl.value = '';
2924
1731
 
2925
- edgeEl.value =
2926
- "";
1732
+ clusterEl.value = '';
2927
1733
 
2928
- state.search =
2929
- "";
1734
+ edgeEl.value = '';
2930
1735
 
2931
- state.cluster =
2932
- "";
1736
+ state.search = '';
2933
1737
 
2934
- state.edge =
2935
- "";
1738
+ state.cluster = '';
2936
1739
 
2937
- state.pathStart =
2938
- null;
1740
+ state.edge = '';
2939
1741
 
2940
- resetPathHighlight();
1742
+ state.pathStart = null;
2941
1743
 
2942
- closeDetails();
1744
+ resetPathHighlight();
2943
1745
 
2944
- applyFilters();
1746
+ closeDetails();
2945
1747
 
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
- ) {
1748
+ applyFilters();
1749
+
1750
+ state.graph.zoomToFit(700, 60);
1751
+ });
1752
+
1753
+ document.addEventListener('keydown', (event) => {
1754
+ if (event.key === 'Escape') {
2961
1755
  closeDetails();
2962
1756
  }
2963
- }
2964
- );
1757
+ });
2965
1758
 
2966
- main().catch(
2967
- error => {
2968
- const loading =
2969
- document.getElementById(
2970
- "loading"
2971
- );
1759
+ main().catch((error) => {
1760
+ const loading = document.getElementById('loading');
2972
1761
 
2973
- if (
2974
- loading
2975
- ) {
2976
- loading.textContent =
2977
- error.message;
1762
+ if (loading) {
1763
+ loading.textContent = error.message;
2978
1764
  }
2979
1765
 
2980
- console.error(
2981
- error
2982
- );
2983
- }
2984
- );
2985
- </script>
2986
- </body>
1766
+ console.error(error);
1767
+ });
1768
+ </script>
1769
+ </body>
2987
1770
  </html>