@flowatlas/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flowatlas.js +4 -0
- package/dist/chunks/chunk-FUKXR2HJ.js +7277 -0
- package/dist/chunks/chunk-QFF3SJXQ.js +6164 -0
- package/dist/chunks/chunk-XBCRZY5O.js +859 -0
- package/dist/chunks/dist-BWLUSWUP.js +43 -0
- package/dist/chunks/watch-CHTDLNUE.js +7 -0
- package/dist/index.js +5639 -0
- package/dist/visualise/page.html +1004 -0
- package/package.json +79 -0
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
<title>__TITLE__</title>
|
|
2
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
4
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
|
5
|
+
|
|
6
|
+
<style>
|
|
7
|
+
:root {
|
|
8
|
+
--ground: #e9ecea;
|
|
9
|
+
--paper: #f6f8f6;
|
|
10
|
+
--raise: #ffffff;
|
|
11
|
+
--ink: #171b19;
|
|
12
|
+
--muted: #5f6d67;
|
|
13
|
+
--faint: #8b9891;
|
|
14
|
+
--rule: #cbd3ce;
|
|
15
|
+
--rule-soft: #dde3df;
|
|
16
|
+
--accent: #0d5c55;
|
|
17
|
+
--accent-soft: #d3e5e1;
|
|
18
|
+
|
|
19
|
+
--good: #3f7d3f;
|
|
20
|
+
--warn: #97701a;
|
|
21
|
+
--bad: #a03626;
|
|
22
|
+
|
|
23
|
+
--s-0: #2f6f8f;
|
|
24
|
+
--s-1: #a2542f;
|
|
25
|
+
--s-2: #5b6e2f;
|
|
26
|
+
--s-3: #7a3f6d;
|
|
27
|
+
--s-4: #b08424;
|
|
28
|
+
|
|
29
|
+
--f-display: 'Fraunces', Georgia, 'Times New Roman', serif;
|
|
30
|
+
--f-ui: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
31
|
+
--f-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
32
|
+
|
|
33
|
+
--gap: 18px;
|
|
34
|
+
--radius: 3px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@media (prefers-color-scheme: dark) {
|
|
38
|
+
:root:not([data-theme='light']) {
|
|
39
|
+
--ground: #101412;
|
|
40
|
+
--paper: #171d1a;
|
|
41
|
+
--raise: #1e2622;
|
|
42
|
+
--ink: #e4e9e6;
|
|
43
|
+
--muted: #94a39c;
|
|
44
|
+
--faint: #6d7b74;
|
|
45
|
+
--rule: #2b3531;
|
|
46
|
+
--rule-soft: #222b27;
|
|
47
|
+
--accent: #5ec7bb;
|
|
48
|
+
--accent-soft: #123833;
|
|
49
|
+
|
|
50
|
+
--good: #7bbb72;
|
|
51
|
+
--warn: #d8ac52;
|
|
52
|
+
--bad: #e08573;
|
|
53
|
+
|
|
54
|
+
--s-0: #6fb2d4;
|
|
55
|
+
--s-1: #e0916a;
|
|
56
|
+
--s-2: #a4bd6a;
|
|
57
|
+
--s-3: #c68fb8;
|
|
58
|
+
--s-4: #e0bd63;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:root[data-theme='dark'] {
|
|
63
|
+
--ground: #101412;
|
|
64
|
+
--paper: #171d1a;
|
|
65
|
+
--raise: #1e2622;
|
|
66
|
+
--ink: #e4e9e6;
|
|
67
|
+
--muted: #94a39c;
|
|
68
|
+
--faint: #6d7b74;
|
|
69
|
+
--rule: #2b3531;
|
|
70
|
+
--rule-soft: #222b27;
|
|
71
|
+
--accent: #5ec7bb;
|
|
72
|
+
--accent-soft: #123833;
|
|
73
|
+
|
|
74
|
+
--good: #7bbb72;
|
|
75
|
+
--warn: #d8ac52;
|
|
76
|
+
--bad: #e08573;
|
|
77
|
+
|
|
78
|
+
--s-0: #6fb2d4;
|
|
79
|
+
--s-1: #e0916a;
|
|
80
|
+
--s-2: #a4bd6a;
|
|
81
|
+
--s-3: #c68fb8;
|
|
82
|
+
--s-4: #e0bd63;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
* { box-sizing: border-box; }
|
|
86
|
+
|
|
87
|
+
body {
|
|
88
|
+
margin: 0;
|
|
89
|
+
background: var(--ground);
|
|
90
|
+
color: var(--ink);
|
|
91
|
+
font-family: var(--f-ui);
|
|
92
|
+
font-size: 14px;
|
|
93
|
+
line-height: 1.5;
|
|
94
|
+
-webkit-font-smoothing: antialiased;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
a { color: var(--accent); }
|
|
98
|
+
|
|
99
|
+
:focus-visible {
|
|
100
|
+
outline: 2px solid var(--accent);
|
|
101
|
+
outline-offset: 2px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.shell {
|
|
105
|
+
max-width: 1500px;
|
|
106
|
+
margin: 0 auto;
|
|
107
|
+
padding: 26px 24px 60px;
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: 22px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── masthead ───────────────────────────────────────────────────────── */
|
|
114
|
+
|
|
115
|
+
.masthead {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-wrap: wrap;
|
|
118
|
+
align-items: flex-end;
|
|
119
|
+
justify-content: space-between;
|
|
120
|
+
gap: 16px;
|
|
121
|
+
padding-bottom: 16px;
|
|
122
|
+
border-bottom: 1px solid var(--rule);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.wordmark {
|
|
126
|
+
font-family: var(--f-display);
|
|
127
|
+
font-size: 30px;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
letter-spacing: -0.015em;
|
|
130
|
+
margin: 0;
|
|
131
|
+
text-wrap: balance;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.standfirst {
|
|
135
|
+
margin: 4px 0 0;
|
|
136
|
+
color: var(--muted);
|
|
137
|
+
max-width: 62ch;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.built {
|
|
141
|
+
font-family: var(--f-mono);
|
|
142
|
+
font-size: 11px;
|
|
143
|
+
color: var(--faint);
|
|
144
|
+
text-align: right;
|
|
145
|
+
line-height: 1.7;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── the reconciliation band ────────────────────────────────────────── */
|
|
149
|
+
|
|
150
|
+
.band {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
|
|
153
|
+
gap: 1px;
|
|
154
|
+
background: var(--rule-soft);
|
|
155
|
+
border: 1px solid var(--rule);
|
|
156
|
+
border-radius: var(--radius);
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.stat {
|
|
161
|
+
background: var(--paper);
|
|
162
|
+
padding: 12px 14px 13px;
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
gap: 3px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.stat dt {
|
|
169
|
+
font-size: 10.5px;
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
letter-spacing: 0.09em;
|
|
172
|
+
color: var(--faint);
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.stat dd {
|
|
177
|
+
margin: 0;
|
|
178
|
+
font-family: var(--f-mono);
|
|
179
|
+
font-size: 21px;
|
|
180
|
+
font-variant-numeric: tabular-nums;
|
|
181
|
+
line-height: 1.15;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.stat small {
|
|
185
|
+
color: var(--muted);
|
|
186
|
+
font-size: 11.5px;
|
|
187
|
+
font-family: var(--f-ui);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.stat.is-good dd { color: var(--good); }
|
|
191
|
+
.stat.is-warn dd { color: var(--warn); }
|
|
192
|
+
|
|
193
|
+
/* ── service rail ───────────────────────────────────────────────────── */
|
|
194
|
+
|
|
195
|
+
.fleet {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-wrap: wrap;
|
|
198
|
+
gap: 8px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.ship {
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: baseline;
|
|
204
|
+
gap: 8px;
|
|
205
|
+
padding: 6px 11px 6px 9px;
|
|
206
|
+
border: 1px solid var(--rule);
|
|
207
|
+
border-left: 3px solid var(--hue);
|
|
208
|
+
border-radius: var(--radius);
|
|
209
|
+
background: var(--paper);
|
|
210
|
+
font-size: 12.5px;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
color: inherit;
|
|
213
|
+
font-family: inherit;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.ship[aria-pressed='true'] {
|
|
217
|
+
background: var(--raise);
|
|
218
|
+
border-color: var(--hue);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.ship[aria-pressed='false'] { opacity: 0.5; }
|
|
222
|
+
|
|
223
|
+
.ship b { font-weight: 600; }
|
|
224
|
+
|
|
225
|
+
.ship span {
|
|
226
|
+
font-family: var(--f-mono);
|
|
227
|
+
font-size: 11px;
|
|
228
|
+
color: var(--muted);
|
|
229
|
+
font-variant-numeric: tabular-nums;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.ship em {
|
|
233
|
+
font-style: normal;
|
|
234
|
+
font-size: 10px;
|
|
235
|
+
text-transform: uppercase;
|
|
236
|
+
letter-spacing: 0.07em;
|
|
237
|
+
color: var(--faint);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ── working area ───────────────────────────────────────────────────── */
|
|
241
|
+
|
|
242
|
+
.work {
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
|
245
|
+
gap: var(--gap);
|
|
246
|
+
align-items: start;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@media (max-width: 900px) {
|
|
250
|
+
.work { grid-template-columns: minmax(0, 1fr); }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.panel {
|
|
254
|
+
border: 1px solid var(--rule);
|
|
255
|
+
border-radius: var(--radius);
|
|
256
|
+
background: var(--paper);
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
min-width: 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.panel-head {
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: space-between;
|
|
266
|
+
gap: 10px;
|
|
267
|
+
padding: 10px 12px;
|
|
268
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
269
|
+
flex-wrap: wrap;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.panel-head h2 {
|
|
273
|
+
font-family: var(--f-display);
|
|
274
|
+
font-size: 15px;
|
|
275
|
+
font-weight: 600;
|
|
276
|
+
margin: 0;
|
|
277
|
+
letter-spacing: -0.01em;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.hint {
|
|
281
|
+
font-size: 11.5px;
|
|
282
|
+
color: var(--faint);
|
|
283
|
+
font-family: var(--f-mono);
|
|
284
|
+
font-variant-numeric: tabular-nums;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
input[type='search'],
|
|
288
|
+
select {
|
|
289
|
+
font-family: var(--f-ui);
|
|
290
|
+
font-size: 13px;
|
|
291
|
+
color: var(--ink);
|
|
292
|
+
background: var(--raise);
|
|
293
|
+
border: 1px solid var(--rule);
|
|
294
|
+
border-radius: var(--radius);
|
|
295
|
+
padding: 6px 9px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
input[type='search'] { width: 100%; }
|
|
299
|
+
|
|
300
|
+
.filters {
|
|
301
|
+
display: flex;
|
|
302
|
+
gap: 8px;
|
|
303
|
+
padding: 10px 12px;
|
|
304
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.list {
|
|
308
|
+
list-style: none;
|
|
309
|
+
margin: 0;
|
|
310
|
+
padding: 0;
|
|
311
|
+
overflow-y: auto;
|
|
312
|
+
max-height: 560px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.list li + li { border-top: 1px solid var(--rule-soft); }
|
|
316
|
+
|
|
317
|
+
.row {
|
|
318
|
+
display: block;
|
|
319
|
+
width: 100%;
|
|
320
|
+
text-align: left;
|
|
321
|
+
background: none;
|
|
322
|
+
border: 0;
|
|
323
|
+
border-left: 3px solid var(--hue);
|
|
324
|
+
padding: 8px 12px;
|
|
325
|
+
cursor: pointer;
|
|
326
|
+
color: inherit;
|
|
327
|
+
font-family: inherit;
|
|
328
|
+
display: flex;
|
|
329
|
+
flex-direction: column;
|
|
330
|
+
gap: 2px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.row:hover { background: var(--raise); }
|
|
334
|
+
.row[aria-current='true'] { background: var(--accent-soft); }
|
|
335
|
+
|
|
336
|
+
.row-name {
|
|
337
|
+
font-family: var(--f-mono);
|
|
338
|
+
font-size: 12px;
|
|
339
|
+
word-break: break-word;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.row-meta {
|
|
343
|
+
font-size: 11px;
|
|
344
|
+
color: var(--faint);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.verb {
|
|
348
|
+
font-weight: 500;
|
|
349
|
+
color: var(--hue);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ── the walk ───────────────────────────────────────────────────────── */
|
|
353
|
+
|
|
354
|
+
.tabs {
|
|
355
|
+
display: flex;
|
|
356
|
+
gap: 2px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.tab {
|
|
360
|
+
font-family: inherit;
|
|
361
|
+
font-size: 12px;
|
|
362
|
+
padding: 5px 11px;
|
|
363
|
+
border: 1px solid var(--rule);
|
|
364
|
+
background: var(--paper);
|
|
365
|
+
color: var(--muted);
|
|
366
|
+
border-radius: var(--radius);
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.tab[aria-selected='true'] {
|
|
371
|
+
background: var(--raise);
|
|
372
|
+
color: var(--ink);
|
|
373
|
+
border-color: var(--accent);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.walk {
|
|
377
|
+
overflow: auto;
|
|
378
|
+
max-height: 620px;
|
|
379
|
+
padding: 4px 0 12px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.step {
|
|
383
|
+
display: grid;
|
|
384
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
385
|
+
align-items: baseline;
|
|
386
|
+
gap: 10px;
|
|
387
|
+
padding: 3px 14px 3px 0;
|
|
388
|
+
padding-left: calc(var(--level) * 15px + 14px);
|
|
389
|
+
white-space: nowrap;
|
|
390
|
+
position: relative;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* One hairline per level of depth, so the shape of the walk is readable
|
|
394
|
+
without spending characters on drawing it. */
|
|
395
|
+
.step[data-level]:not([data-level='0'])::before {
|
|
396
|
+
content: '';
|
|
397
|
+
position: absolute;
|
|
398
|
+
left: calc(var(--level) * 15px + 4px);
|
|
399
|
+
top: 0;
|
|
400
|
+
bottom: 0;
|
|
401
|
+
border-left: 1px solid var(--rule);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.step:hover { background: var(--raise); }
|
|
405
|
+
|
|
406
|
+
.via {
|
|
407
|
+
font-family: var(--f-mono);
|
|
408
|
+
font-size: 11px;
|
|
409
|
+
color: var(--muted);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.via.is-marker { color: var(--warn); }
|
|
413
|
+
.via.is-heuristic { color: var(--warn); }
|
|
414
|
+
.via.is-runtime { color: var(--bad); }
|
|
415
|
+
|
|
416
|
+
.what {
|
|
417
|
+
display: inline-flex;
|
|
418
|
+
align-items: baseline;
|
|
419
|
+
gap: 7px;
|
|
420
|
+
min-width: 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.glyph {
|
|
424
|
+
font-family: var(--f-mono);
|
|
425
|
+
font-size: 9.5px;
|
|
426
|
+
letter-spacing: 0.06em;
|
|
427
|
+
text-transform: uppercase;
|
|
428
|
+
color: var(--raise);
|
|
429
|
+
background: var(--hue);
|
|
430
|
+
border-radius: 2px;
|
|
431
|
+
padding: 1px 4px;
|
|
432
|
+
flex: none;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.label {
|
|
436
|
+
font-family: var(--f-mono);
|
|
437
|
+
font-size: 12.5px;
|
|
438
|
+
overflow: hidden;
|
|
439
|
+
text-overflow: ellipsis;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.where {
|
|
443
|
+
font-size: 11px;
|
|
444
|
+
color: var(--faint);
|
|
445
|
+
font-family: var(--f-mono);
|
|
446
|
+
text-align: right;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.crossing {
|
|
450
|
+
display: flex;
|
|
451
|
+
align-items: center;
|
|
452
|
+
gap: 9px;
|
|
453
|
+
margin: 5px 14px 5px 0;
|
|
454
|
+
padding: 4px 10px;
|
|
455
|
+
border-left: 3px solid var(--hue);
|
|
456
|
+
background: var(--raise);
|
|
457
|
+
border-radius: var(--radius);
|
|
458
|
+
font-size: 11.5px;
|
|
459
|
+
color: var(--muted);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.crossing b {
|
|
463
|
+
font-weight: 600;
|
|
464
|
+
color: var(--hue);
|
|
465
|
+
font-family: var(--f-ui);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.repeat { color: var(--faint); font-style: italic; }
|
|
469
|
+
|
|
470
|
+
.cut {
|
|
471
|
+
margin: 8px 14px 0 0;
|
|
472
|
+
padding: 7px 11px;
|
|
473
|
+
border: 1px dashed var(--rule);
|
|
474
|
+
border-radius: var(--radius);
|
|
475
|
+
font-size: 12px;
|
|
476
|
+
color: var(--muted);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* ── tables ─────────────────────────────────────────────────────────── */
|
|
480
|
+
|
|
481
|
+
.sheet { overflow-x: auto; }
|
|
482
|
+
|
|
483
|
+
table {
|
|
484
|
+
width: 100%;
|
|
485
|
+
border-collapse: collapse;
|
|
486
|
+
font-size: 12.5px;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
th {
|
|
490
|
+
text-align: left;
|
|
491
|
+
font-size: 10.5px;
|
|
492
|
+
text-transform: uppercase;
|
|
493
|
+
letter-spacing: 0.08em;
|
|
494
|
+
color: var(--faint);
|
|
495
|
+
font-weight: 500;
|
|
496
|
+
padding: 8px 12px;
|
|
497
|
+
border-bottom: 1px solid var(--rule);
|
|
498
|
+
white-space: nowrap;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
td {
|
|
502
|
+
padding: 7px 12px;
|
|
503
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
504
|
+
vertical-align: top;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
td.mono {
|
|
508
|
+
font-family: var(--f-mono);
|
|
509
|
+
font-size: 11.5px;
|
|
510
|
+
word-break: break-word;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
td.num {
|
|
514
|
+
font-family: var(--f-mono);
|
|
515
|
+
font-variant-numeric: tabular-nums;
|
|
516
|
+
text-align: right;
|
|
517
|
+
white-space: nowrap;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
tr:hover td { background: var(--raise); }
|
|
521
|
+
|
|
522
|
+
.tag {
|
|
523
|
+
display: inline-block;
|
|
524
|
+
font-family: var(--f-mono);
|
|
525
|
+
font-size: 10.5px;
|
|
526
|
+
padding: 1px 6px;
|
|
527
|
+
border-radius: 2px;
|
|
528
|
+
border: 1px solid var(--rule);
|
|
529
|
+
color: var(--muted);
|
|
530
|
+
white-space: nowrap;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.tag.is-good { color: var(--good); border-color: var(--good); }
|
|
534
|
+
.tag.is-warn { color: var(--warn); border-color: var(--warn); }
|
|
535
|
+
.tag.is-bad { color: var(--bad); border-color: var(--bad); }
|
|
536
|
+
|
|
537
|
+
.legend {
|
|
538
|
+
display: flex;
|
|
539
|
+
flex-wrap: wrap;
|
|
540
|
+
gap: 14px;
|
|
541
|
+
font-size: 11.5px;
|
|
542
|
+
color: var(--muted);
|
|
543
|
+
padding: 10px 12px;
|
|
544
|
+
border-top: 1px solid var(--rule-soft);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.legend span { display: inline-flex; align-items: baseline; gap: 6px; }
|
|
548
|
+
|
|
549
|
+
.colophon {
|
|
550
|
+
color: var(--faint);
|
|
551
|
+
font-size: 12px;
|
|
552
|
+
border-top: 1px solid var(--rule);
|
|
553
|
+
padding-top: 14px;
|
|
554
|
+
max-width: 72ch;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.colophon code {
|
|
558
|
+
font-family: var(--f-mono);
|
|
559
|
+
font-size: 11.5px;
|
|
560
|
+
color: var(--muted);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
@media (prefers-reduced-motion: reduce) {
|
|
564
|
+
* { animation: none !important; transition: none !important; }
|
|
565
|
+
}
|
|
566
|
+
</style>
|
|
567
|
+
|
|
568
|
+
<div class="shell">
|
|
569
|
+
<header class="masthead">
|
|
570
|
+
<div>
|
|
571
|
+
<h1 class="wordmark">__TITLE__</h1>
|
|
572
|
+
<p class="standfirst" id="standfirst"></p>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="built" id="built"></div>
|
|
575
|
+
</header>
|
|
576
|
+
|
|
577
|
+
<dl class="band" id="band"></dl>
|
|
578
|
+
|
|
579
|
+
<div class="fleet" id="fleet"></div>
|
|
580
|
+
|
|
581
|
+
<div class="work">
|
|
582
|
+
<section class="panel">
|
|
583
|
+
<div class="panel-head">
|
|
584
|
+
<h2>Ways in</h2>
|
|
585
|
+
<span class="hint" id="entry-count"></span>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="filters">
|
|
588
|
+
<input type="search" id="search" placeholder="Filter by path, name or service" aria-label="Filter ways in">
|
|
589
|
+
<select id="kind" aria-label="Kind of entry">
|
|
590
|
+
<option value="">All kinds</option>
|
|
591
|
+
</select>
|
|
592
|
+
</div>
|
|
593
|
+
<ul class="list" id="entries"></ul>
|
|
594
|
+
</section>
|
|
595
|
+
|
|
596
|
+
<section class="panel">
|
|
597
|
+
<div class="panel-head">
|
|
598
|
+
<h2 id="walk-title">The walk</h2>
|
|
599
|
+
<div class="tabs" role="tablist">
|
|
600
|
+
<button class="tab" role="tab" data-view="walk" aria-selected="true">Walk</button>
|
|
601
|
+
<button class="tab" role="tab" data-view="crossings" aria-selected="false">Crossings</button>
|
|
602
|
+
<button class="tab" role="tab" data-view="missing" aria-selected="false">Not joined</button>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="filters" id="walk-controls">
|
|
606
|
+
<label class="hint" style="display:flex;align-items:center;gap:6px">
|
|
607
|
+
depth
|
|
608
|
+
<select id="depth">
|
|
609
|
+
<option>3</option><option>4</option><option>5</option>
|
|
610
|
+
<option selected>6</option><option>8</option><option>10</option>
|
|
611
|
+
</select>
|
|
612
|
+
</label>
|
|
613
|
+
<label class="hint" style="display:flex;align-items:center;gap:6px">
|
|
614
|
+
most nodes
|
|
615
|
+
<select id="cap">
|
|
616
|
+
<option>40</option><option selected>90</option><option>200</option><option>500</option>
|
|
617
|
+
</select>
|
|
618
|
+
</label>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="walk" id="walk"></div>
|
|
621
|
+
<div class="legend" id="legend"></div>
|
|
622
|
+
</section>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<p class="colophon" id="colophon"></p>
|
|
626
|
+
</div>
|
|
627
|
+
|
|
628
|
+
<script type="application/json" id="graph">__DATA__</script>
|
|
629
|
+
|
|
630
|
+
<script>
|
|
631
|
+
(() => {
|
|
632
|
+
const data = JSON.parse(document.getElementById('graph').textContent);
|
|
633
|
+
const { types, repos, kinds, files, edgeTypes, confidences } = data.dicts;
|
|
634
|
+
|
|
635
|
+
/* Node fields, as packed: type, label, repo, kind, file, line, meta. A node
|
|
636
|
+
is known by its position; only the ways in keep a name, in entryIds. */
|
|
637
|
+
const TY = 0, LB = 1, RP = 2, KD = 3, FL = 4, LN = 5, MT = 6;
|
|
638
|
+
const nodes = data.nodes;
|
|
639
|
+
|
|
640
|
+
const hueOf = (repoIndex) => `var(--s-${repoIndex % 5})`;
|
|
641
|
+
const repoName = (i) => repos[nodes[i][RP]];
|
|
642
|
+
const typeName = (i) => types[nodes[i][TY]];
|
|
643
|
+
|
|
644
|
+
/* Edges a walk follows outward. Guards hang off the entry, never descended. */
|
|
645
|
+
const FORWARD = new Set(['handles', 'calls', 'queries', 'caches', 'emits', 'consumes',
|
|
646
|
+
'http_calls', 'hits', 'triggers', 'reads_config']);
|
|
647
|
+
const CROSSES = new Set(['http_calls', 'hits']);
|
|
648
|
+
|
|
649
|
+
const out = nodes.map(() => []);
|
|
650
|
+
const guards = nodes.map(() => []);
|
|
651
|
+
for (const e of data.edges) {
|
|
652
|
+
const name = edgeTypes[e[2]];
|
|
653
|
+
if (name === 'guarded_by') guards[e[0]].push(e);
|
|
654
|
+
else if (FORWARD.has(name)) out[e[0]].push(e);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* Short, readable stand-ins for node types; the vocabulary is the subject's. */
|
|
658
|
+
const GLYPH = {
|
|
659
|
+
entry: 'in', method: 'fn', provider: 'cls', db_query: 'db', table: 'tbl',
|
|
660
|
+
channel: 'ch', producer: 'pub', consumer: 'sub', http_out: 'out',
|
|
661
|
+
ui_component: 'view', ui_action: 'act', ui_api_call: 'req',
|
|
662
|
+
config_key: 'cfg', external_api: 'ext', module: 'mod', repo: 'repo',
|
|
663
|
+
guard: 'grd', interceptor: 'int', pipe: 'pipe',
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
/* ── the reconciliation band ─────────────────────────────────────── */
|
|
667
|
+
|
|
668
|
+
const r = data.report;
|
|
669
|
+
const band = document.getElementById('band');
|
|
670
|
+
const stat = (label, value, note, tone) => {
|
|
671
|
+
const el = document.createElement('div');
|
|
672
|
+
el.className = 'stat' + (tone ? ' is-' + tone : '');
|
|
673
|
+
el.innerHTML = `<dt>${label}</dt><dd>${value}</dd><small>${note}</small>`;
|
|
674
|
+
band.append(el);
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
stat('Graph', r.totals.nodes.toLocaleString() + ' nodes',
|
|
678
|
+
r.totals.edges.toLocaleString() + ' edges, ' + r.types.total.toLocaleString() + ' types');
|
|
679
|
+
stat('Service calls', r.httpOut.linked + ' / ' + r.httpOut.total + ' joined',
|
|
680
|
+
r.httpOut.external + ' third party, ' + r.httpOut.dynamic + ' built at run time',
|
|
681
|
+
r.httpOut.linked > 0 ? 'good' : null);
|
|
682
|
+
if (r.ui) {
|
|
683
|
+
stat('Browser calls', r.ui.resolved + ' / ' + r.ui.total + ' joined',
|
|
684
|
+
r.ui.unresolved + ' still unmatched', r.ui.resolved > 0 ? 'warn' : null);
|
|
685
|
+
}
|
|
686
|
+
stat('Routes reached', r.routes.called + ' / ' + r.routes.total,
|
|
687
|
+
(r.routes.total - r.routes.called) + ' nothing calls');
|
|
688
|
+
stat('Channels', r.channels.linked + ' / ' + r.channels.total + ' joined',
|
|
689
|
+
r.channels.noConsumers.length + ' with no handler',
|
|
690
|
+
r.channels.linked === 0 ? 'warn' : 'good');
|
|
691
|
+
stat('Left unresolved', r.totals.unresolved.toLocaleString(),
|
|
692
|
+
'each one recorded with a reason');
|
|
693
|
+
|
|
694
|
+
const read = r.services.filter((s) => s.skipped === null).length;
|
|
695
|
+
document.getElementById('standfirst').textContent =
|
|
696
|
+
`${read} of ${r.services.length} repositories read and joined into one graph. Pick a way in ` +
|
|
697
|
+
'on the left and follow it; a rule across the walk marks every point where it leaves one ' +
|
|
698
|
+
'service for another.';
|
|
699
|
+
|
|
700
|
+
document.getElementById('built').innerHTML =
|
|
701
|
+
'built ' + new Date(data.builtAt).toISOString().replace('T', ' ').slice(0, 19) + ' UTC<br>' +
|
|
702
|
+
r.services.map((s) => s.durationMs + 'ms ' + s.name).join(' · ');
|
|
703
|
+
|
|
704
|
+
/* ── service rail, doubling as the filter ────────────────────────── */
|
|
705
|
+
|
|
706
|
+
const shown = new Set(repos);
|
|
707
|
+
const fleet = document.getElementById('fleet');
|
|
708
|
+
repos.forEach((name, i) => {
|
|
709
|
+
const summary = r.services.find((s) => s.name === name);
|
|
710
|
+
const b = document.createElement('button');
|
|
711
|
+
b.className = 'ship';
|
|
712
|
+
b.style.setProperty('--hue', hueOf(i));
|
|
713
|
+
b.setAttribute('aria-pressed', 'true');
|
|
714
|
+
b.innerHTML = `<b>${name}</b><em>${summary ? summary.type : ''}</em>` +
|
|
715
|
+
`<span>${summary ? summary.nodes.toLocaleString() : '0'} nodes</span>`;
|
|
716
|
+
b.addEventListener('click', () => {
|
|
717
|
+
if (shown.has(name)) shown.delete(name); else shown.add(name);
|
|
718
|
+
b.setAttribute('aria-pressed', String(shown.has(name)));
|
|
719
|
+
drawEntries();
|
|
720
|
+
});
|
|
721
|
+
fleet.append(b);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
/* ── ways in ─────────────────────────────────────────────────────── */
|
|
725
|
+
|
|
726
|
+
const entryIx = [];
|
|
727
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
728
|
+
const t = typeName(i);
|
|
729
|
+
if (t === 'entry' || t === 'ui_action') entryIx.push(i);
|
|
730
|
+
}
|
|
731
|
+
entryIx.sort((a, b) => {
|
|
732
|
+
const ra = repoName(a), rb = repoName(b);
|
|
733
|
+
if (ra !== rb) return ra < rb ? -1 : 1;
|
|
734
|
+
return nodes[a][LB] < nodes[b][LB] ? -1 : 1;
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const kindSelect = document.getElementById('kind');
|
|
738
|
+
for (const k of [...new Set(entryIx.map((i) => kinds[nodes[i][KD]]).filter(Boolean))].sort()) {
|
|
739
|
+
const o = document.createElement('option');
|
|
740
|
+
o.value = k; o.textContent = k;
|
|
741
|
+
kindSelect.append(o);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const search = document.getElementById('search');
|
|
745
|
+
const listEl = document.getElementById('entries');
|
|
746
|
+
const countEl = document.getElementById('entry-count');
|
|
747
|
+
let current = null;
|
|
748
|
+
|
|
749
|
+
const matches = () => {
|
|
750
|
+
const q = search.value.trim().toLowerCase();
|
|
751
|
+
const kind = kindSelect.value;
|
|
752
|
+
return entryIx.filter((i) => {
|
|
753
|
+
if (!shown.has(repoName(i))) return false;
|
|
754
|
+
if (kind && kinds[nodes[i][KD]] !== kind) return false;
|
|
755
|
+
if (!q) return true;
|
|
756
|
+
return nodes[i][LB].toLowerCase().includes(q);
|
|
757
|
+
});
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
function drawEntries() {
|
|
761
|
+
const found = matches();
|
|
762
|
+
countEl.textContent = found.length.toLocaleString() + ' of ' + entryIx.length.toLocaleString();
|
|
763
|
+
listEl.replaceChildren();
|
|
764
|
+
const frag = document.createDocumentFragment();
|
|
765
|
+
for (const i of found.slice(0, 400)) {
|
|
766
|
+
const li = document.createElement('li');
|
|
767
|
+
const b = document.createElement('button');
|
|
768
|
+
b.className = 'row';
|
|
769
|
+
b.style.setProperty('--hue', hueOf(nodes[i][RP]));
|
|
770
|
+
if (i === current) b.setAttribute('aria-current', 'true');
|
|
771
|
+
const meta = nodes[i][MT] || {};
|
|
772
|
+
const name = meta.m
|
|
773
|
+
? `<span class="verb">${meta.m}</span> ${meta.p || ''}`
|
|
774
|
+
: nodes[i][LB];
|
|
775
|
+
b.innerHTML = `<span class="row-name">${name}</span>` +
|
|
776
|
+
`<span class="row-meta">${repoName(i)} · ${kinds[nodes[i][KD]] || typeName(i)}` +
|
|
777
|
+
(files[nodes[i][FL]] ? ' · ' + files[nodes[i][FL]].split('/').pop() : '') + '</span>';
|
|
778
|
+
b.addEventListener('click', () => { current = i; drawEntries(); drawWalk(); });
|
|
779
|
+
li.append(b);
|
|
780
|
+
frag.append(li);
|
|
781
|
+
}
|
|
782
|
+
if (found.length > 400) {
|
|
783
|
+
const li = document.createElement('li');
|
|
784
|
+
li.innerHTML = `<div class="cut" style="margin:8px 12px">${(found.length - 400).toLocaleString()} more. Narrow the filter.</div>`;
|
|
785
|
+
frag.append(li);
|
|
786
|
+
}
|
|
787
|
+
listEl.append(frag);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* ── the walk ────────────────────────────────────────────────────── */
|
|
791
|
+
|
|
792
|
+
const walkEl = document.getElementById('walk');
|
|
793
|
+
const titleEl = document.getElementById('walk-title');
|
|
794
|
+
const depthSel = document.getElementById('depth');
|
|
795
|
+
const capSel = document.getElementById('cap');
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
function drawWalk() {
|
|
799
|
+
if (current === null) return;
|
|
800
|
+
const depth = Number(depthSel.value);
|
|
801
|
+
const cap = Number(capSel.value);
|
|
802
|
+
walkEl.replaceChildren();
|
|
803
|
+
titleEl.textContent = nodes[current][LB];
|
|
804
|
+
|
|
805
|
+
const frag = document.createDocumentFragment();
|
|
806
|
+
let used = 0, over = 0;
|
|
807
|
+
|
|
808
|
+
const emit = (i, level, edge, path) => {
|
|
809
|
+
const row = document.createElement('div');
|
|
810
|
+
row.className = 'step';
|
|
811
|
+
row.style.setProperty('--hue', hueOf(nodes[i][RP]));
|
|
812
|
+
row.style.setProperty('--level', String(level));
|
|
813
|
+
row.dataset.level = String(level);
|
|
814
|
+
const t = typeName(i);
|
|
815
|
+
const meta = nodes[i][MT] || {};
|
|
816
|
+
const label = meta.m ? `${meta.m} ${meta.p || ''}` : nodes[i][LB];
|
|
817
|
+
const conf = edge ? confidences[edge[3]] : '';
|
|
818
|
+
row.innerHTML =
|
|
819
|
+
`<span class="what">` +
|
|
820
|
+
`<span class="glyph">${GLYPH[t] || t.slice(0, 4)}</span>` +
|
|
821
|
+
`<span class="label">${label}</span>` +
|
|
822
|
+
`</span>` +
|
|
823
|
+
`<span class="where">` +
|
|
824
|
+
(edge ? `<span class="via is-${conf}">${edgeTypes[edge[2]]}${conf === 'static' ? '' : ' · ' + conf}</span> ` : '') +
|
|
825
|
+
(files[nodes[i][FL]] ? files[nodes[i][FL]].split('/').pop() + (nodes[i][LN] ? ':' + nodes[i][LN] : '') : '') +
|
|
826
|
+
`</span>`;
|
|
827
|
+
frag.append(row);
|
|
828
|
+
|
|
829
|
+
const guarded = guards[i];
|
|
830
|
+
if (guarded.length > 0) {
|
|
831
|
+
const g = document.createElement('div');
|
|
832
|
+
g.className = 'crossing';
|
|
833
|
+
g.style.setProperty('--hue', hueOf(nodes[i][RP]));
|
|
834
|
+
g.style.marginLeft = (level * 15 + 28) + 'px';
|
|
835
|
+
g.innerHTML = '<b>runs first</b>' + guarded
|
|
836
|
+
.slice()
|
|
837
|
+
.sort((a, b) => a[0] - b[0])
|
|
838
|
+
.map((e) => nodes[e[1]][LB].replace(/\(.*/, ''))
|
|
839
|
+
.join(', ');
|
|
840
|
+
frag.append(g);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const queue = [{ i: current, level: 0, path: new Set([current]), edge: null }];
|
|
845
|
+
emit(current, 0, null);
|
|
846
|
+
used = 1;
|
|
847
|
+
|
|
848
|
+
while (queue.length > 0) {
|
|
849
|
+
const item = queue.shift();
|
|
850
|
+
if (item.level >= depth) continue;
|
|
851
|
+
const edges = out[item.i].slice().sort((a, b) => a[1] - b[1]);
|
|
852
|
+
for (const e of edges) {
|
|
853
|
+
const to = e[1];
|
|
854
|
+
if (item.path.has(to)) {
|
|
855
|
+
if (used < cap) {
|
|
856
|
+
used += 1;
|
|
857
|
+
const row = document.createElement('div');
|
|
858
|
+
row.className = 'step';
|
|
859
|
+
row.style.setProperty('--hue', hueOf(nodes[to][RP]));
|
|
860
|
+
row.style.setProperty('--level', String(item.level + 1));
|
|
861
|
+
row.dataset.level = String(item.level + 1);
|
|
862
|
+
row.innerHTML =
|
|
863
|
+
`<span class="what"><span class="repeat">already on this path: ${nodes[to][LB]}</span></span><span></span>`;
|
|
864
|
+
frag.append(row);
|
|
865
|
+
} else over += 1;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (used >= cap) { over += 1; continue; }
|
|
869
|
+
|
|
870
|
+
if (CROSSES.has(edgeTypes[e[2]])) {
|
|
871
|
+
const cross = document.createElement('div');
|
|
872
|
+
cross.className = 'crossing';
|
|
873
|
+
cross.style.setProperty('--hue', hueOf(nodes[to][RP]));
|
|
874
|
+
cross.style.marginLeft = (item.level * 15 + 28) + 'px';
|
|
875
|
+
cross.innerHTML = `<b>${repoName(item.i)} → ${repoName(to)}</b>` +
|
|
876
|
+
`over ${edgeTypes[e[2]]}, ${confidences[e[3]]}`;
|
|
877
|
+
frag.append(cross);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
used += 1;
|
|
881
|
+
emit(to, item.level + 1, e);
|
|
882
|
+
queue.push({ i: to, level: item.level + 1, path: new Set([...item.path, to]) });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (over > 0) {
|
|
887
|
+
const cut = document.createElement('div');
|
|
888
|
+
cut.className = 'cut';
|
|
889
|
+
cut.textContent = over + ' more nodes. Raise the cap, or narrow the depth.';
|
|
890
|
+
frag.append(cut);
|
|
891
|
+
}
|
|
892
|
+
walkEl.append(frag);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* ── crossings: every edge that leaves a service ─────────────────── */
|
|
896
|
+
|
|
897
|
+
function drawCrossings() {
|
|
898
|
+
titleEl.textContent = 'Every crossing';
|
|
899
|
+
const rows = data.edges
|
|
900
|
+
.filter((e) => CROSSES.has(edgeTypes[e[2]]))
|
|
901
|
+
.sort((a, b) => repoName(a[0]) < repoName(b[0]) ? -1 : 1);
|
|
902
|
+
|
|
903
|
+
const table = document.createElement('table');
|
|
904
|
+
table.innerHTML = '<thead><tr><th>From</th><th>To</th><th>Route</th><th>Trust</th></tr></thead>';
|
|
905
|
+
const body = document.createElement('tbody');
|
|
906
|
+
for (const e of rows) {
|
|
907
|
+
const from = nodes[e[0]], to = nodes[e[1]];
|
|
908
|
+
const meta = to[MT] || {};
|
|
909
|
+
const tr = document.createElement('tr');
|
|
910
|
+
tr.innerHTML =
|
|
911
|
+
`<td class="mono"><span class="tag" style="color:${hueOf(from[RP])};border-color:${hueOf(from[RP])}">${repos[from[RP]]}</span> ${files[from[FL]] ? files[from[FL]].split('/').pop() + ':' + from[LN] : from[LB]}</td>` +
|
|
912
|
+
`<td class="mono"><span class="tag" style="color:${hueOf(to[RP])};border-color:${hueOf(to[RP])}">${repos[to[RP]]}</span></td>` +
|
|
913
|
+
`<td class="mono">${meta.m || ''} ${meta.p || to[LB]}</td>` +
|
|
914
|
+
`<td><span class="tag is-${confidences[e[3]] === 'static' ? 'good' : 'warn'}">${edgeTypes[e[2]]} · ${confidences[e[3]]}</span></td>`;
|
|
915
|
+
body.append(tr);
|
|
916
|
+
}
|
|
917
|
+
table.append(body);
|
|
918
|
+
const sheet = document.createElement('div');
|
|
919
|
+
sheet.className = 'sheet';
|
|
920
|
+
sheet.append(table);
|
|
921
|
+
walkEl.replaceChildren(sheet);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/* ── what did not join, and why ──────────────────────────────────── */
|
|
925
|
+
|
|
926
|
+
function drawMissing() {
|
|
927
|
+
titleEl.textContent = 'What did not join';
|
|
928
|
+
const table = document.createElement('table');
|
|
929
|
+
table.innerHTML = '<thead><tr><th>Reason</th><th class="num">Count</th><th>One of them</th></tr></thead>';
|
|
930
|
+
const body = document.createElement('tbody');
|
|
931
|
+
for (const u of data.unresolved) {
|
|
932
|
+
const tr = document.createElement('tr');
|
|
933
|
+
const tone = u.reason.includes('not-found') ? 'bad' : u.reason.includes('dynamic') ? 'warn' : '';
|
|
934
|
+
tr.innerHTML =
|
|
935
|
+
`<td><span class="tag${tone ? ' is-' + tone : ''}">${u.reason}</span></td>` +
|
|
936
|
+
`<td class="num">${u.count.toLocaleString()}</td>` +
|
|
937
|
+
`<td class="mono">${u.service ? u.service + ' · ' : ''}${u.example}</td>`;
|
|
938
|
+
body.append(tr);
|
|
939
|
+
}
|
|
940
|
+
table.append(body);
|
|
941
|
+
const sheet = document.createElement('div');
|
|
942
|
+
sheet.className = 'sheet';
|
|
943
|
+
sheet.append(table);
|
|
944
|
+
walkEl.replaceChildren(sheet);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* ── wiring ──────────────────────────────────────────────────────── */
|
|
948
|
+
|
|
949
|
+
const controls = document.getElementById('walk-controls');
|
|
950
|
+
let view = 'walk';
|
|
951
|
+
for (const tab of document.querySelectorAll('.tab')) {
|
|
952
|
+
tab.addEventListener('click', () => {
|
|
953
|
+
view = tab.dataset.view;
|
|
954
|
+
for (const other of document.querySelectorAll('.tab')) {
|
|
955
|
+
other.setAttribute('aria-selected', String(other === tab));
|
|
956
|
+
}
|
|
957
|
+
controls.hidden = view !== 'walk';
|
|
958
|
+
if (view === 'walk') drawWalk();
|
|
959
|
+
else if (view === 'crossings') drawCrossings();
|
|
960
|
+
else drawMissing();
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
search.addEventListener('input', drawEntries);
|
|
965
|
+
kindSelect.addEventListener('change', drawEntries);
|
|
966
|
+
depthSel.addEventListener('change', drawWalk);
|
|
967
|
+
capSel.addEventListener('change', drawWalk);
|
|
968
|
+
|
|
969
|
+
document.getElementById('legend').innerHTML =
|
|
970
|
+
'<span><b>Trust</b> static, read from the code · marker, an annotation said so · heuristic, a guess</span>' +
|
|
971
|
+
'<span><b>:param</b> a route hole, so one address matches every id</span>';
|
|
972
|
+
|
|
973
|
+
document.getElementById('colophon').innerHTML =
|
|
974
|
+
'Everything here was read from source, never run. A path shown as <code>/orders/:param/cancel</code> ' +
|
|
975
|
+
'is the route as registered; a caller writes a real id there. Where a call could not be ' +
|
|
976
|
+
'followed, it appears under <b>Not joined</b> with the reason rather than being dropped, which is the ' +
|
|
977
|
+
'difference between a map and a guess.';
|
|
978
|
+
|
|
979
|
+
/* Open on a way in that actually leaves a service rather than on an empty
|
|
980
|
+
shell. Which one that is gets computed from the graph in hand: walk the
|
|
981
|
+
forward edges backwards from every crossing, and take the first way in
|
|
982
|
+
the walk reached. Naming a route here would only ever fit one project. */
|
|
983
|
+
const reachesCrossing = (() => {
|
|
984
|
+
const back = nodes.map(() => []);
|
|
985
|
+
const seen = new Uint8Array(nodes.length);
|
|
986
|
+
const queue = [];
|
|
987
|
+
for (const e of data.edges) {
|
|
988
|
+
const name = edgeTypes[e[2]];
|
|
989
|
+
if (FORWARD.has(name)) back[e[1]].push(e[0]);
|
|
990
|
+
if (CROSSES.has(name) && !seen[e[0]]) { seen[e[0]] = 1; queue.push(e[0]); }
|
|
991
|
+
}
|
|
992
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
993
|
+
for (const prev of back[queue[head]]) {
|
|
994
|
+
if (!seen[prev]) { seen[prev] = 1; queue.push(prev); }
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return seen;
|
|
998
|
+
})();
|
|
999
|
+
|
|
1000
|
+
current = entryIx.find((i) => reachesCrossing[i]) ?? entryIx[0];
|
|
1001
|
+
drawEntries();
|
|
1002
|
+
drawWalk();
|
|
1003
|
+
})();
|
|
1004
|
+
</script>
|