-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLST-status-lifecycle.html
More file actions
933 lines (898 loc) · 85.1 KB
/
Copy pathLST-status-lifecycle.html
File metadata and controls
933 lines (898 loc) · 85.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Moderne LST — Service Blueprint</title>
<style>
:root{
--bg:#f7f6f2; --panel:#ffffff; --ink:#23211c; --ink2:#5f5d56; --ink3:#8a887f;
--line:#e3e0d7; --line2:#cfccc1;
--input:#6b5ce7; --input-bg:#efedfd;
--process:#bf7a12; --process-bg:#fbf0db;
/* Aligned with Moderne design system Status foundation (8 intents). */
--success:#3B8948; --success-bg:#F3FFF5; /* status/success */
--info:#2F42FF; --info-bg:#F2F3FF; /* status/info */
--fail:#CB3446; --fail-bg:#FFEDEF; /* status/error */
--warning:#BD9319; --warning-bg:#FEF9E8; /* status/warning (gold ramp — replaces old amber) */
--queued:#AD59C7; --queued-bg:#F6EEFB; /* status/queued (violet ramp) */
--active:#992FB9; --active-bg:#F6EEFB; /* status/active (violet ramp, spinner) */
--neutral:#6B7280; --neutral-bg:#F3F4F6; /* status/neutral (cool grey) */
--canceled:#7B7263; --canceled-bg:#FAF8F3; /* status/canceled (warm grey — distinct namespace) */
--shadow:0 1px 2px rgba(0,0,0,.06),0 2px 8px rgba(0,0,0,.05);
}
@media (prefers-color-scheme: dark){
:root{
--bg:#1b1a17; --panel:#262420; --ink:#ece9e1; --ink2:#b4b1a7; --ink3:#86837a;
--line:#3a3833; --line2:#4a473f;
--input:#a99bff; --input-bg:#2c2945;
--process:#e0a443; --process-bg:#3a3018;
--success:#74c98c; --success-bg:#1c3322;
--info:#7d8bff; --info-bg:#1c2140;
--fail:#ef8d82; --fail-bg:#3a201c;
--warning:#e0b53d; --warning-bg:#3a3018;
--queued:#c98cdb; --queued-bg:#2f213a;
--active:#b466cc; --active-bg:#2f213a;
--neutral:#a9a79e; --neutral-bg:#2f2d28;
--canceled:#a8a09a; --canceled-bg:#2c2926;
--shadow:0 1px 2px rgba(0,0,0,.3),0 2px 10px rgba(0,0,0,.35);
}
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
line-height:1.5;-webkit-font-smoothing:antialiased;}
.page{padding:28px clamp(16px,4vw,48px) 64px;}
header.top{max-width:1100px;}
h1{font-size:24px;font-weight:600;margin:0 0 6px;letter-spacing:-.01em;}
.sub{color:var(--ink2);font-size:15px;margin:0 0 18px;max-width:760px;}
.legend{display:flex;flex-wrap:wrap;gap:14px 20px;margin:0 0 8px;font-size:13px;color:var(--ink2);}
.legend .it{display:flex;align-items:center;gap:7px;}
.dot{width:11px;height:11px;border-radius:3px;flex:none;}
.hint{font-size:13px;color:var(--ink3);margin:10px 0 18px;}
.scroll{overflow-x:auto;border:1px solid var(--line);border-radius:14px;background:var(--panel);box-shadow:var(--shadow);}
.grid{display:grid;grid-template-columns:208px repeat(7,minmax(264px,1fr));min-width:2056px;}
.gh{position:sticky;top:0;z-index:3;background:var(--panel);
border-bottom:2px solid var(--line2);padding:14px 14px 12px;font-weight:600;font-size:13.5px;}
.gh.cli{background:linear-gradient(180deg,color-mix(in srgb,var(--process-bg) 60%,var(--panel)),var(--panel));border-bottom-color:var(--process);}
.gh.platform{background:linear-gradient(180deg,color-mix(in srgb,var(--info-bg) 55%,var(--panel)),var(--panel));border-bottom-color:var(--info);}
.gh.corner{position:sticky;left:0;z-index:4;color:var(--ink3);font-weight:600;
border-right:1px solid var(--line2);background:var(--panel);border-bottom:2px solid var(--line2);}
.gh .pnum{display:inline-block;min-width:20px;height:20px;line-height:20px;text-align:center;border-radius:6px;
background:var(--neutral);color:#fff;font-weight:700;font-size:12px;margin-right:4px;}
.gh.cli .pnum{background:var(--process);} .gh.platform .pnum{background:var(--info);}
.zoneband{padding:9px 14px;font-weight:700;font-size:12px;letter-spacing:.05em;text-transform:uppercase;text-align:center;border-bottom:1px solid var(--line);}
.zoneband.zone-cli{background:color-mix(in srgb,var(--process-bg) 72%,var(--panel));color:var(--process);}
.zoneband.zone-platform{background:color-mix(in srgb,var(--info-bg) 66%,var(--panel));color:var(--info);}
.zbcorner{position:sticky;left:0;z-index:2;background:var(--panel);border-right:1px solid var(--line2);border-bottom:1px solid var(--line);padding:9px 14px;font-size:11.5px;color:var(--ink3);font-weight:600;}
.zbound{border-right:2px dashed var(--line2) !important;}
.gh.zbound{border-right:2px dashed var(--process) !important;}
.zoneband.zone-cli{border-right:2px dashed var(--process);}
.lane-label{position:sticky;left:0;z-index:2;background:var(--panel);border-right:1px solid var(--line2);
border-bottom:1px solid var(--line);border-left:5px solid var(--neutral);padding:14px 14px 14px 12px;font-weight:600;font-size:13.5px;display:flex;flex-direction:column;justify-content:center;gap:3px;}
.cell.statuscell{display:flex;align-items:center;}
.lane-label small{font-weight:400;color:var(--ink3);font-size:11.5px;}
.lane-label.lane-evidence{border-left-color:var(--input);background:color-mix(in srgb,var(--input-bg) 55%,var(--panel));}
.lane-label.lane-user{border-left-color:var(--success);background:color-mix(in srgb,var(--success-bg) 55%,var(--panel));}
.lane-label.lane-frontstage{border-left-color:var(--info);background:color-mix(in srgb,var(--info-bg) 55%,var(--panel));}
.lane-label.lane-backstage{border-left-color:var(--process);background:color-mix(in srgb,var(--process-bg) 55%,var(--panel));}
.lane-label.lane-support{border-left-color:var(--neutral);background:color-mix(in srgb,var(--neutral-bg) 70%,var(--panel));}
.lane-label.lane-gaps{border-left-color:var(--fail);background:color-mix(in srgb,var(--fail-bg) 60%,var(--panel));}
.cell{border-bottom:1px solid var(--line);border-right:1px solid var(--line);padding:9px;}
.row-even .cell,.row-even .lane-label{background:color-mix(in srgb,var(--bg) 45%,var(--panel));}
.card{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--neutral);
border-radius:9px;padding:9px 11px;cursor:pointer;height:100%;transition:transform .08s,box-shadow .12s,filter .12s;}
.card:hover{box-shadow:var(--shadow);transform:translateY(-1px);filter:saturate(1.15) brightness(1.02);}
.card:focus-visible{outline:2px solid var(--info);outline-offset:2px;}
.card .t{font-weight:600;font-size:13.5px;margin:0 0 3px;display:flex;align-items:center;gap:6px;}
.card .t .pip{width:9px;height:9px;border-radius:50%;flex:none;box-shadow:0 0 0 3px rgba(255,255,255,.35);}
.card .s{font-size:12px;margin:0;opacity:.92;color:var(--ink2);}
.card .pip{display:none;}
.card.cl-evidence{background:var(--input-bg);border-color:var(--input);border-left-color:var(--input);}
.card.cl-evidence .t{color:var(--input);}
.card.cl-user{background:var(--success-bg);border-color:var(--success);border-left-color:var(--success);}
.card.cl-user .t{color:var(--success);}
.card.cl-frontstage{background:var(--info-bg);border-color:var(--info);border-left-color:var(--info);}
.card.cl-frontstage .t{color:var(--info);}
.card.cl-backstage{background:var(--process-bg);border-color:var(--process);border-left-color:var(--process);}
.card.cl-backstage .t{color:var(--process);}
.card.cl-support{background:var(--neutral-bg);border-color:var(--line2);border-left-color:var(--neutral);}
.card.cl-support .t{color:var(--ink);}
.empty{color:var(--ink3);font-size:12px;font-style:italic;padding:9px 11px;}
.line-row{grid-column:1 / -1;display:flex;align-items:center;gap:12px;
padding:7px 16px;background:color-mix(in srgb,var(--bg) 60%,var(--panel));
border-bottom:1px dashed var(--line2);border-top:1px dashed var(--line2);
font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);font-weight:600;
position:sticky;left:0;}
.line-row::before,.line-row::after{content:"";height:1px;background:var(--line2);flex:1;}
.line-row.gap{background:var(--fail-bg);color:var(--fail);border-color:transparent;}
/* drawer */
.backdrop{position:fixed;inset:0;background:rgba(0,0,0,.34);opacity:0;pointer-events:none;transition:opacity .18s;z-index:20;}
.backdrop.on{opacity:1;pointer-events:auto;}
.drawer{position:fixed;top:0;right:0;height:100%;width:min(420px,92vw);background:var(--panel);
box-shadow:-8px 0 30px rgba(0,0,0,.18);transform:translateX(100%);transition:transform .2s;z-index:21;
display:flex;flex-direction:column;}
.drawer.on{transform:none;}
.drawer .dh{padding:20px 22px 14px;border-bottom:1px solid var(--line);}
.drawer .badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;
padding:3px 9px;border-radius:999px;margin-bottom:10px;}
.drawer .ctx{font-size:12px;color:var(--ink3);margin:0 0 6px;}
.drawer h2{font-size:18px;margin:0;font-weight:600;}
.drawer .db{padding:18px 22px;overflow-y:auto;font-size:14.5px;color:var(--ink2);}
.drawer .db p{margin:0 0 12px;}
.drawer .close{position:absolute;top:14px;right:14px;border:none;background:transparent;color:var(--ink3);
font-size:22px;cursor:pointer;line-height:1;padding:6px;border-radius:8px;}
.drawer .close:hover{background:var(--neutral-bg);color:var(--ink);}
/* status chips — Moderne DS Badge spec (medium): 24px tall, padding 2px 10px, font 12px / 500, line-height 1, gap 4px, 14px leading-icon (stroke 1.5), 6px dot, 1px transparent-shade border. */
.chips{display:flex;flex-wrap:wrap;gap:8px 10px;}
.chip{display:inline-flex;align-items:center;gap:4px;height:24px;padding:2px 10px;font-family:inherit;font-weight:500;font-size:12px;line-height:1;letter-spacing:0;border-radius:999px;border:1px solid transparent;white-space:nowrap;box-sizing:border-box;}
.chip .sw{width:6px;height:6px;border-radius:50%;flex:none;}
.chip .badgeicon{width:14px;height:14px;flex:none;color:currentColor;display:inline-flex;align-items:center;justify-content:center;}
.chip .badgeicon svg{width:100%;height:100%;stroke:currentColor;stroke-width:1.5;fill:none;stroke-linecap:round;stroke-linejoin:round;}
.chip .badgeicon.spin svg{animation:activespin 4s linear infinite;transform-box:fill-box;transform-origin:50% 50%;}
@keyframes activespin{to{transform:rotate(360deg);}}
@media (prefers-reduced-motion: reduce){.chip .badgeicon.spin svg{animation:none;}}
.chip.click{cursor:pointer;font-family:inherit;}
.chip.click:hover{filter:brightness(1.06) saturate(1.1);box-shadow:var(--shadow);}
.pbar{flex:1 0 100%;display:flex;flex-direction:column;gap:5px;min-width:150px;background:none;border:none;padding:0;margin:0 0 3px;cursor:pointer;text-align:left;}
.pbar .track{height:8px;border-radius:5px;background:color-mix(in srgb,var(--neutral) 22%,transparent);overflow:hidden;}
.pbar .fill{height:100%;border-radius:5px;}
.pbar .fill.blue{background-color:var(--active);} .pbar .fill.green{background-color:var(--success);} .pbar .fill.red{background-color:var(--fail);}
.pbar .fill.striped{background-image:linear-gradient(45deg,rgba(255,255,255,.4) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.4) 50%,rgba(255,255,255,.4) 75%,transparent 75%,transparent);background-size:13px 13px;animation:pbarstripe .9s linear infinite;}
@keyframes pbarstripe{to{background-position:13px 0;}}
.pbar .pends{display:flex;justify-content:space-between;align-items:center;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--ink3);}
.pbar .pends span:nth-child(2){color:var(--info);font-weight:700;}
.pbar .plabel{font-size:11px;color:var(--ink2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
@media (prefers-reduced-motion: reduce){.pbar .fill.striped{animation:none;}}
.strs{display:flex;flex-direction:column;gap:3px;align-items:flex-start;}
.strval{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--ink);background:none;border:none;padding:0;margin:0;cursor:pointer;text-align:left;line-height:1.4;}
.strval:hover{text-decoration:underline;}
.chip .warn{color:var(--process);font-size:11px;font-weight:700;margin-left:3px;}
.chip.warned{outline:2px solid var(--process);outline-offset:1px;font-weight:700;animation:warnpulse 2.2s ease-in-out infinite;}
@keyframes warnpulse{0%,100%{box-shadow:0 0 0 1px color-mix(in srgb,var(--process) 38%,transparent);}50%{box-shadow:0 0 0 7px color-mix(in srgb,var(--process) 14%,transparent);}}
@media (prefers-reduced-motion: reduce){.chip.warned{animation:none;}}
/* Chip colour classes — bg = --{intent}-bg (light shade), border = 60%-alpha (transparent shade), text/icon = --{intent} (default shade). Dot = solid default. */
.c-grey,.c-neutral{background:var(--neutral-bg);color:var(--neutral);border-color:color-mix(in srgb,var(--neutral) 40%,transparent);} .c-grey .sw,.c-neutral .sw{background:var(--neutral);}
.c-green,.c-success{background:var(--success-bg);color:var(--success);border-color:color-mix(in srgb,var(--success) 40%,transparent);} .c-green .sw,.c-success .sw{background:var(--success);}
.c-red,.c-error{background:var(--fail-bg);color:var(--fail);border-color:color-mix(in srgb,var(--fail) 40%,transparent);} .c-red .sw,.c-error .sw{background:var(--fail);}
.c-amber,.c-warning,.c-gold{background:var(--warning-bg);color:var(--warning);border-color:color-mix(in srgb,var(--warning) 40%,transparent);} .c-amber .sw,.c-warning .sw,.c-gold .sw{background:var(--warning);}
.c-blue,.c-info{background:var(--info-bg);color:var(--info);border-color:color-mix(in srgb,var(--info) 40%,transparent);} .c-blue .sw,.c-info .sw{background:var(--info);}
.c-queued{background:var(--queued-bg);color:var(--queued);border-color:color-mix(in srgb,var(--queued) 40%,transparent);} .c-queued .sw{background:var(--queued);}
.c-active{background:var(--active-bg);color:var(--active);border-color:color-mix(in srgb,var(--active) 40%,transparent);} .c-active .sw{background:var(--active);}
.c-canceled{background:var(--canceled-bg);color:var(--canceled);border-color:color-mix(in srgb,var(--canceled) 40%,transparent);} .c-canceled .sw{background:var(--canceled);}
.c-none{background:var(--neutral-bg);color:var(--ink3);border-color:transparent;} .c-none .sw{background:transparent;border:1px dashed var(--ink3);}
/* Legacy "today" chip look — the pre-DS-alignment style used before this pass. Larger padding, 600-weight,
9 px dot instead of a Lucide icon, original hex palette. Applied only to the today half of the
split "today | proposed" cells. */
.chip.legacy{height:auto;padding:3px 9px;font-weight:600;gap:6px;border-color:transparent;}
.chip.legacy .badgeicon{display:none;}
.chip.legacy .sw{width:9px;height:9px;}
.chip.legacy.c-grey,.chip.legacy.c-neutral{background:color-mix(in srgb,#8a887f 18%,var(--panel));color:var(--ink2);border-color:transparent;} .chip.legacy.c-grey .sw,.chip.legacy.c-neutral .sw{background:#8a887f;}
.chip.legacy.c-green,.chip.legacy.c-success{background:#e7f4ea;color:#2f8f4e;border-color:transparent;} .chip.legacy.c-green .sw,.chip.legacy.c-success .sw{background:#2f8f4e;}
.chip.legacy.c-red,.chip.legacy.c-error{background:#fbe9e7;color:#c0392b;border-color:transparent;} .chip.legacy.c-red .sw,.chip.legacy.c-error .sw{background:#c0392b;}
.chip.legacy.c-amber,.chip.legacy.c-warning{background:#fbf0db;color:#bf7a12;border-color:transparent;} .chip.legacy.c-amber .sw,.chip.legacy.c-warning .sw{background:#d99a2b;}
.chip.legacy.c-blue,.chip.legacy.c-info{background:#e6f0fa;color:#2f6fb0;border-color:transparent;} .chip.legacy.c-blue .sw,.chip.legacy.c-info .sw{background:#2f6fb0;}
.chip.legacy.c-gold{background:color-mix(in srgb,#c9a227 20%,var(--panel));color:#a07f0f;border-color:transparent;} .chip.legacy.c-gold .sw{background:#c9a227;}
@media (prefers-color-scheme: dark){
.chip.legacy.c-green,.chip.legacy.c-success{background:#1c3322;color:#74c98c;}
.chip.legacy.c-red,.chip.legacy.c-error{background:#3a201c;color:#ef8d82;} .chip.legacy.c-red .sw,.chip.legacy.c-error .sw{background:#ef8d82;}
.chip.legacy.c-amber,.chip.legacy.c-warning{background:#3a3018;color:#e0a443;} .chip.legacy.c-amber .sw,.chip.legacy.c-warning .sw{background:#e0a443;}
.chip.legacy.c-blue,.chip.legacy.c-info{background:#16293c;color:#7db4ec;} .chip.legacy.c-blue .sw,.chip.legacy.c-info .sw{background:#7db4ec;}
}
.line-row.toggle-line{cursor:pointer;user-select:none;color:var(--ink2);}
.line-row.toggle-line .toggle{color:var(--ink2);margin-right:5px;}
.line-row.toggle-line .tlhint{color:var(--ink3);font-weight:400;text-transform:none;letter-spacing:0;margin-left:7px;}
.line-row.toggle-line:hover{background:color-mix(in srgb,var(--info-bg) 32%,var(--panel));}
.hiddenrow{display:none !important;}
.lane-label.lane-cli_status{border-left-color:var(--neutral);background:color-mix(in srgb,var(--neutral-bg) 70%,var(--panel));}
.lane-label.lane-plat_status{border-left-color:var(--neutral);background:color-mix(in srgb,var(--neutral-bg) 70%,var(--panel));}
.cell.splitcell{display:flex;align-items:stretch;padding:0;}
.splitcell .half{flex:1 1 0;min-width:0;overflow:hidden;display:flex;flex-direction:column;justify-content:center;gap:5px;padding:9px 10px;}
.splitcell .chip{white-space:normal;max-width:100%;}
.splitcell .half.today{border-right:1px dashed var(--line2);}
.splitcell .half.prop{background:color-mix(in srgb,var(--success-bg) 16%,transparent);}
.splitcell .hlabel{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ink3);}
.splitcell .half.prop .hlabel{color:var(--success);}
.splitcell .chip{font-size:11px;padding:2px 7px;}
.splitcell .pbar{min-width:0;}
.viewh{font-size:19px;font-weight:600;margin:22px 0 4px;}
.viewlead{color:var(--ink2);font-size:14px;margin:0 0 14px;max-width:820px;}
.flowcard{border:1px solid var(--line);border-radius:14px;background:var(--panel);box-shadow:var(--shadow);padding:16px;overflow-x:auto;}
svg.flow{display:block;width:100%;min-width:640px;margin:0 auto;height:auto;font-family:inherit;}
/* responsive flow */
.flow2{position:relative;border:1px solid var(--line);border-radius:14px;background:var(--panel);box-shadow:var(--shadow);padding:0;}
.flow2-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:5;font-family:inherit;}
.flow2-overlay text{paint-order:stroke fill;stroke:var(--panel);stroke-width:4px;stroke-linecap:round;stroke-linejoin:round;font-weight:500;}
.flow2-stack{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));grid-template-rows:auto 1fr;column-gap:clamp(6px,.8vw,16px);padding:0 clamp(10px,1.2vw,18px) 18px;position:relative;}
.f2-band{grid-row:1;padding:10px 14px;font-weight:700;font-size:12px;letter-spacing:.04em;margin-top:18px;border:1px dashed;border-bottom:none;border-radius:10px 10px 0 0;}
.f2-band.cli{grid-column:1 / span 6;color:var(--process);background:color-mix(in srgb,var(--process-bg) 55%,var(--panel));border-color:var(--process);}
.f2-band.platform{grid-column:7 / span 5;color:var(--info);background:color-mix(in srgb,var(--info-bg) 55%,var(--panel));border-color:var(--info);}
.f2-zone{grid-row:2;border:1px dashed;border-top:none;border-radius:0 0 12px 12px;z-index:0;}
.f2-zone.cli{grid-column:1 / span 6;border-color:var(--process);background:color-mix(in srgb,var(--process-bg) 30%,var(--panel));}
.f2-zone.platform{grid-column:7 / span 5;border-color:var(--info);background:color-mix(in srgb,var(--info-bg) 30%,var(--panel));}
.f2-col{grid-row:2;display:grid;grid-template-rows:minmax(54px,auto) auto minmax(96px,auto);align-items:center;justify-items:center;row-gap:14px;min-width:0;z-index:1;padding:16px 4px 18px;}
.f2-aux-top,.f2-aux-bottom{display:flex;flex-direction:column;gap:6px;align-items:center;width:100%;min-width:0;}
.f2-aux-top{justify-content:flex-end;align-self:end;}
.f2-aux-bottom{justify-content:flex-start;align-self:start;}
.f2-main{display:flex;align-items:center;justify-content:center;width:100%;min-width:0;padding:0 4px;}
.f2-node{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-weight:600;font-size:12.5px;line-height:1.2;padding:8px 10px;background:var(--panel);border:1.5px solid var(--line2);position:relative;z-index:2;max-width:100%;min-width:0;box-sizing:border-box;}
.f2-node small{font-weight:400;font-size:10px;color:var(--ink2);margin-top:3px;display:block;line-height:1.25;}
.f2-node b{display:block;}
.f2-node.pill{border-radius:999px;width:100%;}
.f2-node.rect{border-radius:10px;width:100%;}
.f2-node.small{font-size:11px;padding:5px 9px;width:auto;max-width:100%;font-weight:600;}
.f2-node.input{background:var(--input-bg);border-color:var(--input);color:var(--input);}
.f2-node.success{background:var(--success-bg);border-color:var(--success);color:var(--success);}
.f2-node.process{background:var(--process-bg);border-color:var(--process);color:var(--process);}
.f2-node.info{background:var(--info-bg);border-color:var(--info);color:var(--info);}
.f2-node.fail{background:var(--fail-bg);border-color:var(--fail);color:var(--fail);}
.f2-node.gold{background:color-mix(in srgb,#c9a227 20%,var(--panel));border-color:#c9a227;color:#a07f0f;}
.f2-node.neutral{background:var(--neutral-bg);border-color:var(--neutral);color:var(--ink2);}
.f2-diamond{position:relative;width:100%;max-width:130px;aspect-ratio:148/104;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:11.5px;line-height:1.15;text-align:center;z-index:2;}
.f2-diamond svg{position:absolute;inset:0;width:100%;height:100%;}
.f2-diamond.gold svg polygon{fill:color-mix(in srgb,#c9a227 16%,var(--panel));stroke:#c9a227;stroke-width:1.5;}
.f2-diamond.gold{color:#a07f0f;}
.f2-diamond > span{position:relative;max-width:72%;}
.f2-cylinder{position:relative;width:100%;max-width:160px;height:56px;background:var(--neutral-bg);border:1.3px solid var(--neutral);border-radius:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-weight:600;color:var(--ink);font-size:12.5px;z-index:2;box-sizing:border-box;padding:6px 10px;}
.f2-cylinder small{font-weight:400;font-size:10px;color:var(--ink2);margin-top:3px;}
.layoutwrap{border:1px solid var(--line);border-radius:14px;background:var(--panel);box-shadow:var(--shadow);padding:18px;overflow-x:auto;}
pre.tree{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.75;margin:0;white-space:pre;color:var(--ink2);}
pre.tree .dir{color:var(--info);font-weight:600;}
pre.tree .v3{color:var(--success);font-weight:600;}
pre.tree .file{color:var(--ink);}
pre.tree .ann{color:var(--ink3);}
.noterow{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px;}
.noterow .n{flex:1;min-width:240px;border:1px solid var(--line);border-radius:10px;padding:11px 13px;font-size:13.5px;color:var(--ink2);line-height:1.5;}
.noterow .n b{color:var(--ink);font-weight:600;}
.drawer .db code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;background:var(--neutral-bg);padding:1px 5px;border-radius:5px;color:var(--ink);}
.drawer .db strong{color:var(--ink);}
footer{max-width:1100px;margin-top:26px;font-size:13px;color:var(--ink2);}
footer h3{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink3);margin:0 0 8px;}
footer a{color:var(--info);text-decoration:none;word-break:break-all;}
footer a:hover{text-decoration:underline;}
footer li{margin-bottom:4px;}
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute;overflow:hidden" aria-hidden="true">
<defs>
<!-- Lucide 24×24 viewBoxes; rendered at 14px inside the badge, stroke set from CSS. -->
<symbol id="dsicon-circle-check" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></symbol>
<symbol id="dsicon-circle-minus" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/></symbol>
<symbol id="dsicon-circle-x" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></symbol>
<symbol id="dsicon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol>
<symbol id="dsicon-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></symbol>
<symbol id="dsicon-loader-circle" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></symbol>
<symbol id="dsicon-octagon-alert" viewBox="0 0 24 24"><path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"/><path d="M12 8v4"/><path d="M12 16h.01"/></symbol>
<symbol id="dsicon-triangle-alert" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></symbol>
</defs>
</svg>
<div class="page">
<header class="top">
<h1>Moderne LST — service blueprint</h1>
<p class="sub">From registered repo to finished recipe run. Phases 1–4 are per-repo (long-lived); 5–7 fire on every run. Row statuses in phases 5–6 reflect the post-PR #1518 world where Sync and State are independent (see callout below).</p>
<div class="legend" id="legend"></div>
</header>
<h2 class="viewh">Lifecycle by layer</h2>
<p class="viewlead">Rows are layers, columns are phases. The <strong>Platform UI status</strong> row splits today | <span style="color:var(--success);font-weight:500">proposed</span>. Click any cell or chip for detail; toggle rows to expand CLI status and Engine & Storage.</p>
<div class="scroll">
<div class="grid" id="grid"></div>
</div>
<div style="border:1px solid var(--info);border-radius:var(--border-radius-lg);padding:12px;margin-top:14px;background:color-mix(in srgb,var(--info-bg) 32%,var(--panel));">
<div style="font-weight:600;margin-bottom:8px;display:flex;align-items:center;gap:7px;"><span style="color:var(--info)">▸</span> PR #1518 · Sync and State are now independent</div>
<div style="display:flex;flex-direction:column;gap:6px;font-size:14px;line-height:1.45;color:var(--ink2)">
<div><strong style="color:var(--ink)">Sync is no longer a prerequisite for State.</strong> Previously safe assumption <em>“State advances only after Sync=SYNCED”</em> is now false. Any code that reads <code style="font-family:ui-monospace,monospace">syncStatus</code> to decide whether to render / enable the State column must be relaxed.</div>
<div><strong style="color:var(--ink)">Event ordering can interleave.</strong> <code style="font-family:ui-monospace,monospace">RepoSynced</code> and <code style="font-family:ui-monospace,monospace">RepoRunFinished</code> arrive in either order for the same repo. Row-derivation logic that assumes sync-then-run must handle run-then-sync too — check <code style="font-family:ui-monospace,monospace">repository-results.rows.ts</code> and <code style="font-family:ui-monospace,monospace">changeset-status.helpers.ts</code>.</div>
<div><strong style="color:var(--ink)">resultsCount gating.</strong> <code style="font-family:ui-monospace,monospace">isRepositoryNotYetRun(row.state)</code> currently returns <code style="font-family:ui-monospace,monospace">-</code> for pre-run states. Confirm <code style="font-family:ui-monospace,monospace">SYNCING</code> is still treated as “not yet run” for legacy rows, and that a row in <code style="font-family:ui-monospace,monospace">RUNNING</code> with Sync=<code style="font-family:ui-monospace,monospace">PENDING</code> doesn't render a stale count.</div>
<div><strong style="color:var(--ink)">Error messages are capped server-side.</strong> The worker truncates <code style="font-family:ui-monospace,monospace">RepoRunFinished.errorMessage</code> at <code style="font-family:ui-monospace,monospace">PIPE_BUF</code>. Nothing to change in the UI, but tooltip / popover copy-full-message is now working on a shorter payload.</div>
<div><strong style="color:var(--ink)">State filter valueOptions omits SYNCING.</strong> Correct for new runs. If the backend can return <code style="font-family:ui-monospace,monospace">SYNCING</code> on legacy rows, those rows are unfilterable — decide whether to add SYNCING back as a legacy-only option.</div>
<div><strong style="color:var(--ink)">Cancel-file consolidated.</strong> <code style="font-family:ui-monospace,monospace">RecipeRunService.cancel</code> now writes one cancel file, not two. The Sync=<code style="font-family:ui-monospace,monospace">CANCELED</code> status still exists on repos.</div>
<div><strong style="color:var(--ink)">Run-level <code style="font-family:ui-monospace,monospace">SYNCING</code> is @deprecated.</strong> New runs jump straight to <code style="font-family:ui-monospace,monospace">RUNNING</code>. Consumers of <code style="font-family:ui-monospace,monospace">OrganizationRecipeRunSyncing</code> — <code style="font-family:ui-monospace,monospace">state-status-cell</code> (run header), <code style="font-family:ui-monospace,monospace">results-progress-bar</code>, <code style="font-family:ui-monospace,monospace">changeset-status.helpers</code>, <code style="font-family:ui-monospace,monospace">use-chat-recipe-run-status</code>, <code style="font-family:ui-monospace,monospace">recent-activity-neo.rows</code> — should keep the SYNCING branch for legacy / in-flight only.</div>
</div>
</div>
<div style="border:1px solid var(--process);border-radius:var(--border-radius-lg);padding:12px;margin-top:14px;">
<div style="font-weight:600;margin-bottom:8px;display:flex;align-items:center;gap:7px;"><span style="color:var(--process)">⚠</span> Gaps the proposed model doesn't resolve on its own</div>
<div style="display:flex;flex-direction:column;gap:6px;font-size:14px;line-height:1.45;color:var(--ink2)">
<div><strong style="color:var(--ink)">No build / refresh trigger</strong> — <code style="font-family:ui-monospace,monospace">requestRepositoryLstDetails</code> has no client caller; the UI is a viewer.</div>
<div><strong style="color:var(--ink)">No build diagnostics</strong> — clone / parse / build / publish logs live only in CI; the UI can't say <em>why</em> a repo failed.</div>
<div><strong style="color:var(--ink)">Staleness is unreliable</strong> — <code style="font-family:ui-monospace,monospace">Repository.changeset</code> is the last-observed SCM HEAD, not a live one, so "N commits behind" would frequently lie. The proposed model drops "Stale" for this reason.</div>
</div>
</div>
<h2 class="viewh">Proposed status colour model</h2>
<p class="viewlead">Aligned with the Moderne <a href="https://moderneinc.github.io/design-system/?path=/docs/foundations-status--documentation" style="color:var(--info)">design system Status foundation</a> — eight intents, semantic tokens <code style="font-family:ui-monospace,monospace">--status-{intent}-{light|default|dark}</code>.</p>
<div class="layoutwrap" style="padding:16px">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;font-size:13.5px">
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--success);margin-top:2px"></span><div><strong style="color:var(--ink)">Success</strong><br><span style="color:var(--ink2);font-size:12.5px">Finished · Synced · Available · Merged · Approved</span></div></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--queued);margin-top:2px"></span><div><strong style="color:var(--ink)">Queued</strong> <span style="color:var(--ink3);font-size:11.5px">· clock icon</span><br><span style="color:var(--ink2);font-size:12.5px">Queued · Pending · Awaiting work</span></div></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--active);margin-top:2px"></span><div><strong style="color:var(--ink)">Active</strong> <span style="color:var(--ink3);font-size:11.5px">· spinner (open arc)</span><br><span style="color:var(--ink2);font-size:12.5px">Running · Processing · Syncing · In progress</span></div></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--info);margin-top:2px"></span><div><strong style="color:var(--ink)">Info</strong><br><span style="color:var(--ink2);font-size:12.5px">Open (PR) · Feature labels · Metadata</span></div></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--warning);margin-top:2px"></span><div><strong style="color:var(--ink)">Warning</strong> <span style="color:var(--ink3);font-size:11.5px">· gold (was amber)</span><br><span style="color:var(--ink2);font-size:12.5px">No LST · LST out of date · Partially parsed · Needs approval</span></div></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--fail);margin-top:2px"></span><div><strong style="color:var(--ink)">Error</strong><br><span style="color:var(--ink2);font-size:12.5px">Error · Failed · Timeout · Blocked</span></div></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--neutral);margin-top:2px"></span><div><strong style="color:var(--ink)">Neutral</strong> <span style="color:var(--ink3);font-size:11.5px">· cool grey</span><br><span style="color:var(--ink2);font-size:12.5px">Unknown · Draft · No changes · Not required · Closed</span></div></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:none;width:16px;height:16px;border-radius:4px;background:var(--canceled);margin-top:2px"></span><div><strong style="color:var(--ink)">Canceled</strong> <span style="color:var(--ink3);font-size:11.5px">· warm grey · own namespace</span><br><span style="color:var(--ink2);font-size:12.5px">Canceled (distinct from Neutral so “draft” vs “canceled” read differently)</span></div></div>
</div>
<div style="margin-top:14px;display:flex;flex-direction:column;gap:8px">
<div style="padding:9px 12px;border-radius:var(--border-radius-lg);background:color-mix(in srgb,var(--info-bg) 40%,var(--panel));font-size:13.5px;color:var(--ink2);line-height:1.5"><strong style="color:var(--info)">Sync collapses into Run.</strong> One status column. Queued covers fetch + slot-wait; Failed → Error, Skipped → No LST, Canceled stays Canceled.</div>
<div style="padding:9px 12px;border-radius:var(--border-radius-lg);background:color-mix(in srgb,var(--info-bg) 40%,var(--panel));font-size:13.5px;color:var(--ink2);line-height:1.5"><strong style="color:var(--info)">Indeterminate progress.</strong> Running uses the Active intent (violet, spinning open-arc) — no percentage, we can't compute reliable per-repo progress. Terminal outcome replaces the spinner.</div>
<div style="padding:9px 12px;border-radius:var(--border-radius-lg);background:color-mix(in srgb,var(--info-bg) 40%,var(--panel));font-size:13.5px;color:var(--ink2);line-height:1.5"><strong style="color:var(--info)">Queued and Active share the violet ramp.</strong> Same intent family — Queued (clock) → Active (spinner) reads as a single motion story. Canceled uses a separate warm-grey ramp so it does not blend into Neutral / Draft rows.</div>
</div>
<h3 style="margin:22px 0 6px;font-size:15px;font-weight:600;color:var(--ink)">Badge spec</h3>
<p style="margin:0 0 10px;font-size:13px;color:var(--ink2);line-height:1.5">Chips in this prototype render at the DS Badge <strong>medium</strong> size (Figma "Badge" node 4091:17230). The Figma file defines only one size; small / large scale proportionally for cases layered on top.</p>
<table style="width:100%;border-collapse:collapse;font-size:12.5px;margin-bottom:10px">
<thead>
<tr style="text-align:left;color:var(--ink3);font-size:11px;text-transform:uppercase;letter-spacing:.05em">
<th style="padding:6px 8px;border-bottom:1px solid var(--line);font-weight:600">Size</th>
<th style="padding:6px 8px;border-bottom:1px solid var(--line);font-weight:600">Height</th>
<th style="padding:6px 8px;border-bottom:1px solid var(--line);font-weight:600">Padding</th>
<th style="padding:6px 8px;border-bottom:1px solid var(--line);font-weight:600">Font</th>
<th style="padding:6px 8px;border-bottom:1px solid var(--line);font-weight:600">Sample</th>
</tr>
</thead>
<tbody>
<tr><td style="padding:8px;color:var(--ink2)">small</td><td style="padding:8px;color:var(--ink2)">20 px</td><td style="padding:8px;color:var(--ink2)">1 px 8 px</td><td style="padding:8px;color:var(--ink2)">10 px / 500</td><td style="padding:8px"><span class="chip c-success" style="height:20px;padding:1px 8px;font-size:10px"><span class="badgeicon" style="width:12px;height:12px" aria-hidden="true"><svg><use href="#dsicon-circle-check"/></svg></span>Success</span></td></tr>
<tr><td style="padding:8px;color:var(--ink);font-weight:600">medium <span style="color:var(--ink3);font-weight:400">· Figma spec</span></td><td style="padding:8px;color:var(--ink2)">24 px</td><td style="padding:8px;color:var(--ink2)">2 px 10 px</td><td style="padding:8px;color:var(--ink2)">12 px / 500</td><td style="padding:8px"><span class="chip c-success"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-circle-check"/></svg></span>Success</span></td></tr>
<tr><td style="padding:8px;color:var(--ink2)">large</td><td style="padding:8px;color:var(--ink2)">28 px</td><td style="padding:8px;color:var(--ink2)">3 px 12 px</td><td style="padding:8px;color:var(--ink2)">14 px / 500</td><td style="padding:8px"><span class="chip c-success" style="height:28px;padding:3px 12px;font-size:14px;gap:6px"><span class="badgeicon" style="width:16px;height:16px" aria-hidden="true"><svg><use href="#dsicon-circle-check"/></svg></span>Success</span></td></tr>
</tbody>
</table>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px 16px;font-size:12.5px;color:var(--ink2);line-height:1.5;margin-bottom:14px">
<div><strong style="color:var(--ink)">Radius</strong> · <code style="font-family:ui-monospace,monospace">radius-full</code> (pill)</div>
<div><strong style="color:var(--ink)">Border</strong> · 1 px solid, <code style="font-family:ui-monospace,monospace">--status-{intent}-transparent</code> (60% alpha — softer than the solid default)</div>
<div><strong style="color:var(--ink)">Text + icon colour</strong> · <code style="font-family:ui-monospace,monospace">--status-{intent}-dark</code></div>
<div><strong style="color:var(--ink)">Background</strong> · <code style="font-family:ui-monospace,monospace">--status-{intent}-light</code></div>
<div><strong style="color:var(--ink)">Line-height</strong> · 1 (text-box-trim: cap → alphabetic centres label between pill caps)</div>
<div><strong style="color:var(--ink)">Gap</strong> · 4 px between icon / dot and label</div>
<div><strong style="color:var(--ink)">Leading icon slot</strong> · 14 px box, stroke 1.5, currentColor</div>
<div><strong style="color:var(--ink)">Dot (icon-less variant)</strong> · 6 px, filled <code style="font-family:ui-monospace,monospace">--status-{intent}-default</code></div>
</div>
<h3 style="margin:18px 0 6px;font-size:15px;font-weight:600;color:var(--ink)">Icon per intent</h3>
<p style="margin:0 0 10px;font-size:13px;color:var(--ink2);line-height:1.5">Lucide outline names — Queued (<code style="font-family:ui-monospace,monospace">clock</code>) and Active (<code style="font-family:ui-monospace,monospace">loader-circle</code>) are <strong>mandatory</strong> because they share the violet ramp; nothing else disambiguates them. Active is the one outline-only intent — the open-arc loader cannot fill — and spins at <code style="font-family:ui-monospace,monospace">4s linear infinite</code> honouring <code style="font-family:ui-monospace,monospace">prefers-reduced-motion</code>. For standalone status icons outside a badge: minimum 16 px, filled silhouette only at 16–17 px, outline at 18 px+.</p>
<div style="display:flex;flex-wrap:wrap;gap:10px 12px;">
<span class="chip c-success"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-circle-check"/></svg></span>Success · circle-check</span>
<span class="chip c-queued"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-clock"/></svg></span>Queued · clock <em style="font-style:normal;color:var(--queued);opacity:.75">(mandatory)</em></span>
<span class="chip c-active"><span class="badgeicon spin" aria-hidden="true"><svg><use href="#dsicon-loader-circle"/></svg></span>Active · loader-circle <em style="font-style:normal;color:var(--active);opacity:.75">(mandatory · spin)</em></span>
<span class="chip c-info"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-info"/></svg></span>Info · info</span>
<span class="chip c-warning"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-triangle-alert"/></svg></span>Warning · triangle-alert</span>
<span class="chip c-error"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-octagon-alert"/></svg></span>Error · octagon-alert</span>
<span class="chip c-neutral"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-circle-minus"/></svg></span>Neutral · circle-minus</span>
<span class="chip c-canceled"><span class="badgeicon" aria-hidden="true"><svg><use href="#dsicon-circle-x"/></svg></span>Canceled · circle-x</span>
</div>
</div>
<h2 class="viewh">User flow</h2>
<div class="flow2" id="flow2" role="img" aria-label="LST user flow as a single horizontal spine with a CLI segment on the left and a Platform UI segment on the right, with a dashed handoff link at the artifact repo and branches above and below the spine">
<svg class="flow2-overlay" id="flow2Overlay" aria-hidden="true">
<defs>
<marker id="f2-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M2 1 L8 5 L2 9" fill="none" stroke="context-stroke" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
</defs>
</svg>
<div class="flow2-stack">
<header class="f2-band cli">CLI · ADMIN / INGEST</header>
<header class="f2-band platform">PLATFORM UI · BROWSE & RUN</header>
<div class="f2-zone cli"></div>
<div class="f2-zone platform"></div>
<div class="f2-col" style="grid-column:1">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-node pill input" data-fid="cli-start"><b>repos.csv</b><small>start · repos + orgs</small></div></div>
<div class="f2-aux-bottom"></div>
</div>
<div class="f2-col" style="grid-column:2">
<div class="f2-aux-top"><div class="f2-node pill success small" data-fid="cli-reuse">Reuse</div></div>
<div class="f2-main"><div class="f2-diamond gold" data-fid="cli-rebuild"><svg viewBox="0 0 148 104" preserveAspectRatio="none"><polygon points="74,4 144,52 74,100 4,52"/></svg><span>Rebuild?</span></div></div>
<div class="f2-aux-bottom"></div>
</div>
<div class="f2-col" style="grid-column:3">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-node rect process" data-fid="cli-build"><b>mod build</b><small>clone·parse·build·publish</small></div></div>
<div class="f2-aux-bottom"></div>
</div>
<div class="f2-col" style="grid-column:4">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-diamond gold" data-fid="cli-buildok"><svg viewBox="0 0 148 104" preserveAspectRatio="none"><polygon points="74,4 144,52 74,100 4,52"/></svg><span>Build OK?</span></div></div>
<div class="f2-aux-bottom"><div class="f2-node rect fail small" data-fid="cli-failed">FAILED</div></div>
</div>
<div class="f2-col" style="grid-column:5">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-node rect process" data-fid="cli-publish"><b>Publish -ast.jar</b><small>→ artifact repo</small></div></div>
<div class="f2-aux-bottom"></div>
</div>
<div class="f2-col" style="grid-column:6">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-cylinder" data-fid="cli-artifact"><b>Artifact repo</b><small>cold storage</small></div></div>
<div class="f2-aux-bottom"></div>
</div>
<div class="f2-col" style="grid-column:7">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-diamond gold" data-fid="plat-lst"><svg viewBox="0 0 148 104" preserveAspectRatio="none"><polygon points="74,4 144,52 74,100 4,52"/></svg><span>LST<br>available?</span></div></div>
<div class="f2-aux-bottom"><div class="f2-node rect process small" data-fid="plat-nolst-1"><b>No LST</b><small>skip · no run</small></div></div>
</div>
<div class="f2-col" style="grid-column:8">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-node rect neutral" data-fid="plat-queued"><b>Queued</b><small>fetch LST or wait for slot</small></div></div>
<div class="f2-aux-bottom"></div>
</div>
<div class="f2-col" style="grid-column:9">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-node rect info" data-fid="plat-run"><b>Running</b><small>visitor chain</small></div></div>
<div class="f2-aux-bottom"></div>
</div>
<div class="f2-col" style="grid-column:10">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-diamond gold" data-fid="plat-result"><svg viewBox="0 0 148 104" preserveAspectRatio="none"><polygon points="74,4 144,52 74,100 4,52"/></svg><span>Recipe<br>result?</span></div></div>
<div class="f2-aux-bottom">
<div class="f2-node rect fail small" data-fid="plat-error">Error</div>
<div class="f2-node rect neutral small" data-fid="plat-canceled-2">Canceled</div>
</div>
</div>
<div class="f2-col" style="grid-column:11">
<div class="f2-aux-top"></div>
<div class="f2-main"><div class="f2-node pill success" data-fid="plat-end"><b>Review / commit</b><small>end · patches → PR</small></div></div>
<div class="f2-aux-bottom"></div>
</div>
</div>
</div>
<footer>
<h3>Sources</h3>
<ul>
<li><a href="https://docs.moderne.io/administrator-documentation/moderne-platform/references/lossless-semantic-trees/">Lossless semantic trees — Moderne Docs</a></li>
<li><a href="https://docs.moderne.io/administrator-documentation/moderne-platform/how-to-guides/mass-ingest">Setting up mass ingest — Moderne Docs</a></li>
<li><a href="https://docs.moderne.io/user-documentation/moderne-cli/references/repos-csv/">Creating a repos.csv file — Moderne Docs</a></li>
<li><a href="https://github.com/moderneinc/mass-ingest-example/blob/main/README.md">mass-ingest-example README — GitHub</a></li>
<li>Internal V3 / v2-sandbox lifecycle notes (Axis A & B states, UI gaps)</li>
<li>Moderne CLI repo — build / run / publish pipeline, storage layout, LstCache, V3 codec (CLAUDE.md notes)</li>
</ul>
</footer>
</div>
<div class="backdrop" id="backdrop"></div>
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="dTitle">
<button class="close" id="dClose" aria-label="Close">×</button>
<div class="dh">
<span class="badge" id="dBadge"></span>
<p class="ctx" id="dCtx"></p>
<h2 id="dTitle"></h2>
</div>
<div class="db" id="dBody"></div>
</aside>
<script>
const CATS={
input:["Input",getCss("--input"),getCss("--input-bg")],
process:["Process",getCss("--process"),getCss("--process-bg")],
success:["Available / success",getCss("--success"),getCss("--success-bg")],
info:["In-flight",getCss("--info"),getCss("--info-bg")],
fail:["Failure / risk",getCss("--fail"),getCss("--fail-bg")],
neutral:["Neutral",getCss("--neutral"),getCss("--neutral-bg")]
};
function getCss(v){return getComputedStyle(document.documentElement).getPropertyValue(v).trim();}
const PHASES=[
"1 · Onboard & register","2 · Build LST","3 · Publish & store",
"4 · Available","5 · Run: sync","6 · Run: execute","7 · Review & persist"
];
const ROWS=[
{type:"toggleline",id:"clilayer",name:"CLI status"},
{type:"lane",id:"cli_status",name:"CLI status",note:"LstManager.State enum",group:"clilayer"},
{type:"lane",id:"plat_status",name:"Platform UI status",note:"today | proposed"},
{type:"line",name:"How it works"},
{type:"lane",id:"evidence",name:"Surfaces",note:"CLI output & platform UI"},
{type:"lane",id:"user",name:"User & commands",note:"who runs what"},
{type:"lane",id:"frontstage",name:"Platform app",note:"UI & API layer"},
{type:"toggleline",id:"eslayer",name:"Engine & Storage"},
{type:"lane",id:"backstage",name:"Engine",note:"worker · CLI runtime",group:"eslayer"},
{type:"lane",id:"support",name:"Storage & external",note:"store · artifact repo · SCM",group:"eslayer"}
];
// cells[lane][phaseIndex] = {t,s,cat,d} (null = empty)
const C={
evidence:[
{t:"repos.csv",s:"cloneUrl · branch · origin · path · org*",cat:"input",d:"The central config listing repositories and their org hierarchy. Also supports a publishUri column pointing at a pre-built LST. One file drives both registration and mass ingestion."},
{t:"CLI logs & metrics",s:"mod build output · Grafana",cat:"neutral",d:"During ingestion the CLI emits build logs and a metrics endpoint; the optional observability stack (Prometheus + Grafana) tracks throughput and failures."},
{t:"-ast.jar",s:"in artifact repo · published date",cat:"process",d:"The serialized LST artifact (historical “-ast.jar” name — same thing as the LST) lands in your Maven-format artifact repository, stamped with a publish timestamp."},
{t:"Repositories grid",s:"“LST available” · published · “Unknown” badge",cat:"success",d:"The org & repositories grid shows lstArtifact.available as a boolean column plus the published date. available:false renders an “Unknown” badge. A toolbar toggle “Include not ingested” filters server-side on available _eq true."},
{t:"Sync chips",s:"Pending · Synced · Failed · Skipped · Canceled",cat:"info",d:"Per-repo RepositorySyncStatus chips: grey Pending, green Synced, red Failed, amber Skipped, grey/gold Canceled. After PR #1518 these chips no longer track the row's state 1:1 — Pending can coexist with a top-level RUNNING run, and SYNCED can arrive after RepoRunFinished."},
{t:"Status column",s:"Queued · Running · Finished · Error · No LST · (Syncing legacy)",cat:"info",d:"Run-phase status derived from the RepositoryChangeset union typename. After PR #1518, new rows skip SYNCING and go straight QUEUED → RUNNING; SYNCING remains only as a legacy mapping for historical / in-flight runs (and is not in the state filter's valueOptions). The run header shows aggregate counts like “180/220 synced”."},
{t:"Change list / patches",s:"search.patch · fix.patch · diffs",cat:"neutral",d:"The changes the recipe derived from the LST surface as a reviewable change list (Platform) or patch files (CLI); you commit or open a PR to push them to the git repo (GitHub / GitLab / …). The LST itself is never pushed."}
],
user:[
{t:"Admin registers repos",s:"SCM connect / org JSON import",cat:"input",d:"An admin registers repositories via an SCM connection (server-side enumeration) or a user-org JSON import (createUserOrganization) — often generated from the repos.csv org columns."},
{t:"Admin builds LSTs",s:"runs mass-ingest (Docker, ~daily)",cat:"process",d:"The admin runs the mass-ingest container (manual, cron, Docker Compose, or VM batch) on a schedule — typically daily. Alternatively a user triggers requestRepositoryLstDetails for a single repo from the UI."},
{t:"Admin publishes artifacts",s:"configures creds → artifact repo",cat:"process",d:"The admin sets PUBLISH_URL and credentials; the pipeline publishes the -ast.jar. Diagnostics validate write/read/overwrite/delete before a real run."},
{t:"User checks availability",s:"browses grid, confirms LST available",cat:"success",d:"A user browses the repositories grid, optionally toggles “Include not ingested”, and confirms repositories have an available LST and a recent published date."},
{t:"User starts a run",s:"runs a recipe across an org",cat:"info",d:"A user launches a recipe run against an organization; the platform begins per-repo sync."},
{t:"User monitors the run",s:"watches status update (3s)",cat:"neutral",d:"The user watches the run page update via the 3-second heartbeat as repositories move Queued → Running → a terminal state."},
{t:"User reviews & commits",s:"review → commit / PR to git",cat:"neutral",d:"The recipe used the LST to compute the changes; the user reviews that change list and commits or opens PRs to push them to the git repo (GitHub / GitLab / …). Customer success diagnoses stuck builds or failed syncs using the grid, badges, and status signals."}
],
frontstage:[
{t:"Org service / connector",s:"creates repo records + hierarchy",cat:"process",d:"The Connector / Org service turns SCM enumeration or imported JSON into repository records and an organization hierarchy inside the platform."},
{t:"Repository details polling",s:"build spinner · poll finishedAt",cat:"process",d:"The repository details page polls worker.repositoryArtifact until finishedAt is set. BUILDING is inferred from “buildId active, finishedAt null” — there is no dedicated status enum on v2-sandbox."},
null,
{t:"Repositories grid",s:"renders available / published · filter",cat:"success",d:"The grid reads lstArtifact { available, published } from the EnrichedRepository fragment and renders the column, badge, and date; the toolbar toggle filters server-side."},
{t:"Run page · sync",s:"sync chips · heartbeat 3s",cat:"info",d:"The run page renders sync chips. A lightweight heartbeat query polls {__typename, id, lastUpdatedAt} every 3000ms and refetches the full results on change. Sync takes precedence: the tree-view shows the sync phase until SYNCED."},
{t:"Run page · status",s:"status column · marker overlays",cat:"info",d:"The status column maps the RepositoryChangeset typename to a label and colour; finished rows can show search-marker overlays."},
{t:"Results view",s:"per-repo diffs · commit UI",cat:"neutral",d:"The results view presents per-repository changes for review alongside commit / PR controls."}
],
backstage:[
{t:"SCM enumeration",s:"createUserOrganization",cat:"process",d:"Server-side SCM enumeration and/or the createUserOrganization mutation establish the repository and org records that back registration."},
{t:"mod build",s:"clone → parse → build → publish (per repo)",cat:"process",d:"A pipeline of BuildSteps (Gradle/Maven/Bazel/SBT/JS/Python): per repo it clones, parses, builds (full Java build + dependency resolution + type attribution), and publishes. BuildJob orchestrates; BuildPartition parallelises; LstWriter.create() picks the V2/V3 writer. Each step writes its own logs (ingest.csv, per-repo clone/parse/build logs) on the CI/CLI side — none of which is surfaced in the SaaS UI. The platform only ever sees the result: lstArtifact.available + published."},
{t:"Serialize + publish",s:"LstJarFile → JAR (uncompressed)",cat:"process",d:"The LST is serialized into the V3 store, then Publish packs the build dir into a JAR via LstJarFile.assembleFrom() — .tree/.bin/.lst stored uncompressed (already compact). The -ast.jar is pushed to the artifact repo and the transport URI is recorded in publish.properties."},
{t:"repositoryArtifact metadata",s:"buildTool · changeset · javaVersion · file counts",cat:"success",d:"Once built, the worker’s repositoryArtifact query returns provenance: build tool & version, git eol/autocrlf, OS, the changeset (commit SHA the LST was built against), java version & code style, and source-file counts by type."},
{t:"Fetch + decrypt",s:"download -ast.jar · PENDING→SYNCED",cat:"info",d:"For each run the worker downloads the LST from cold storage and decrypts it; RepositorySyncStatus moves PENDING → SYNCED (or FAILED / SKIPPED / CANCELED)."},
{t:"Recipe engine",s:"LstReader · mmap · visitor chain",cat:"info",d:"mod run iterates sources via LstReader (V2LstReader = batched segments; V3LstReader = per-source .tree, lazy decompress). V3 memory-maps types.bin / markers.bin via MemorySegment/Arena and decompresses bodies on demand; TAG_CLASS_REF resolves through the ChainResolver (source set + JDK + dependencies); styles auto-detected post-deserialize via StyleAccumulator. The visitor chain runs, skipping files via a precondition, advancing the RepositoryChangeset union. MCP tools (FindMethods, AnalyzeCode, ChangeMethodName, …) consume the same store via LstManager + LstCache."},
{t:"Discard + emit",s:"modified LST discarded · patches",cat:"fail",d:"After the run the modified LST is discarded and your code is untouched; the engine emits patches / a change list. Nothing persists to the stored LST unless it is re-ingested."}
],
support:[
{t:"SCM + GraphQL API",s:"GitHub / GitLab / … · org service",cat:"neutral",d:"Source-control providers plus the GraphQL API and Org service back the registration step."},
{t:"Build infra",s:"VM batch / Docker · JDKs · Maven/Gradle · dep repos",cat:"neutral",d:"Compute for builds scales by repo count: single container → Docker Compose → AWS/GCP/Azure Batch. Needs JDKs, build tools, and reachable dependency repositories."},
{t:"Artifact repository",s:"Maven · Artifactory · S3 · Azure · npm/Pip/Go",cat:"neutral",d:"Durable cold storage for -ast.jar artifacts (a dedicated repository is recommended). Supported transports: Maven (synthetic layout), Artifactory, S3, Azure, npm/Pip/Go. Tenant ingestion then happens server-side via the org connector."},
{t:"GraphQL federation",s:"lstArtifact { available, published }",cat:"neutral",d:"The federated graph exposes the lstArtifact fields via the EnrichedRepository fragment that the grid consumes."},
{t:"Cold storage + heartbeat",s:"fetch · polling interval 3000ms",cat:"neutral",d:"Sync pulls from cold storage; the run page polls on GRAPHQL_POLLING_INTERVAL (3000ms) and stops on a terminal or persistent-error state."},
{t:"V3 format + RPC",s:"per-source .tree · shared type/marker tables",cat:"neutral",d:"The V3 substrate: one .tree per source plus shared markers.bin and types.bin, enabling incremental updates and lazy reads (V2 has neither). Recent work: block-compressed type tables, compact-string mirroring (Latin-1 vs UTF-16LE) to skip UTF-8 transcode, a stable tag registry, and an edit-side type overlay that leaves source-set tables untouched when recipes make no changes. Cross-language runners exchange trees via the Rewrite RPC protocol — not V3, which optimises for durable random-access reads."},
{t:"Persist + re-ingest",s:"commit / rebase · republish LST",cat:"neutral",d:"Committed changes are rebased on commit; the next ingestion job republishes a fresh LST at a newer changeset. Running recipes regularly keeps drift small."}
],
gaps:[
null,
{t:"Build failure isn’t a state",s:"silent revert · no progress",cat:"fail",d:"A failed BUILDING → AVAILABLE transition isn’t modelled. The repository simply returns to “No artifact”, and there is no “30% parsed” or queue-depth progress signal — the poll only reports finished or not-finished."},
null,
{t:"Opaque availability",s:"staleness · eviction vs never-built · no prompt",cat:"fail",d:"available:false can’t distinguish “never built” from “built then evicted” (main’s UNAVAILABLE state was dropped in v2). Staleness vs current HEAD isn’t shown though both changesets exist, and there’s no “you’re stale, re-ingest” prompt."},
{t:"Runtime artifact loss",s:"FAILED · FAILED_LOAD_AST",cat:"fail",d:"Sync FAILED is the run-time discovery that the artifact is gone or corrupt between “available” and “needed for this run”. The same condition can surface later as FAILED_LOAD_AST once the run phase begins."},
null,
{t:"Changes can be lost",s:"commit + re-ingest required",cat:"fail",d:"Results don’t persist into future runs unless they are committed and the next ingestion republishes the LST, so a stored LST can drift behind HEAD."}
]
};
const STATUSES={
cli_status:[
[],
[
{label:"BUILDING",color:"gold",value:"BUILDING",field:"LstManager.State",where:"MCP · tray ◐ · toolbrowser",d:"Still being built. RecipeRunner: “Use lst_status to check progress, retry once READY.” LstCache.validate() picks reuse / incremental / full rebuild here (see flow branch)."},
{label:"PARTIALLY_READY",color:"gold",value:"PARTIALLY_READY",field:"LstManager.State",where:"MCP · tray ◐ · toolbrowser",d:"Some build steps unfinished — store only partially built. LST-backed MCP tools require PARTIALLY_READY or READY."},
{label:"READY",color:"green",value:"READY",field:"LstManager.State",where:"MCP · tray ● · toolbrowser",d:"Built and loadable; tools can read it."},
{label:"FAILED",color:"red",value:"FAILED",field:"LstManager.State",where:"MCP · tray ○ · toolbrowser",d:"Build failed. RecipeRunner surfaces the reason, or points to .moderne/mcp/build.log."}
],
[{label:"READY",color:"green",value:"READY (artifact then published)",field:"LstManager.State / Publish",where:"CLI",d:"The LST state stays READY; Publish assembles the -ast.jar and pushes it to the artifact repo (publish.properties records the URI)."}],
[{label:"READY",color:"green",value:"READY (loadable)",field:"LstManager.State / LstCache",where:"MCP · toolbrowser",d:"The local / cached LST is READY for tools and mod run. Platform equivalent: Available."}],
[{label:"READY",color:"green",value:"READY (no CLI sync phase)",field:"LstManager.State",where:"CLI",d:"The CLI has no sync phase — that is a platform concept. Locally the LST is simply READY when mod run goes to read it."}],
[{label:"READY",color:"green",value:"READY (read by mod run)",field:"LstManager.State",where:"CLI (mod run)",d:"mod run reads the READY LST and runs the recipe; there is no separate CLI run-status string — the LST state stays READY."}],
[]
],
plat_status:[
[],
[],
[],
[{label:"LST available",color:"green",value:"available: true · published: <date>",field:"lstArtifact.available / .published",where:"Org & repositories grid",warn:"‘Published’ tells you when the LST was built, not whether the repo has moved since — and we can't reliably compute staleness from inside the platform (Repository.changeset is the SCM HEAD as last observed, which may itself be stale; nothing polls the SCM continuously). So a drifted LST looks identical to a fresh one. There is also no in-UI build/refresh action (requestRepositoryLstDetails has no client caller).",d:"Worker reported finishedAt; the grid shows the green available column and a published date."}],
[
{label:"Pending",color:"grey",value:"PENDING",field:"RepositorySyncStatus",where:"Sync column chip",warn:"After PR #1518 PENDING can coexist with the run being RUNNING at the top level (under --sync-csv). A row may sit in Sync=Pending while other rows already stream Finished. Any UI logic that assumed “Pending means nothing else is happening yet” is now wrong.",d:"Fetch not yet initiated or in flight. Under --sync-csv, PENDING can now coexist with a top-level RUNNING run — do not treat Pending as a global gate."},
{label:"Synced",color:"green",value:"SYNCED",field:"RepositorySyncStatus",where:"Sync column chip",warn:"After PR #1518 SYNCED can arrive after RepoRunFinished for the same row (concurrent monitors, interleaved events). Downstream code must not gate the State column on Sync=SYNCED.",d:"LST fetched and decrypted. Event ordering is no longer guaranteed vs. the run phase — SYNCED may land after the same row's FINISHED. Treat syncStatus and state as independent."},
{label:"Failed",color:"red",value:"FAILED",field:"RepositorySyncStatus",where:"Sync column chip · tooltip “Sync failed”",d:"Fetch or decryption failed. Downstream State typically becomes ERROR or NO_LST — verify both fields are handled independently, since sync/state are no longer strictly sequenced."},
{label:"Skipped",color:"amber",value:"SKIPPED",field:"RepositorySyncStatus",where:"Sync column chip",d:"No LST to fetch (Axis-A state was No artifact). Row will typically show State=NO_LST."},
{label:"Canceled",color:"grey",value:"CANCELED",field:"RepositorySyncStatus",where:"Sync column chip",d:"Run canceled before sync finished. PR #1518 removed the sync cancel-file (RecipeRunService.cancel now writes one cancel file, not two), but the CANCELED status value still exists on repos."}
],
[
{label:"Queued",color:"grey",value:"QUEUED",field:"RepositoryRecipeRunQueued",where:"Status column",d:"LST ready, recipe scheduled but not started."},
{label:"Syncing",color:"gold",value:"SYNCING (legacy)",field:"ChangesetStatus",where:"Status column · legacy runs only",warn:"After PR #1518 SYNCING is effectively dead for new rows — per-repo state no longer passes through a distinct syncing phase; repos transition straight to RUNNING. Keep the mapping for historical / in-flight runs, but expect it to be rare. Also note: the state filter valueOptions list does not include SYNCING — if the backend can return SYNCING on legacy rows, users cannot filter for those rows.",d:"Legacy-only. Previously a distinct pre-run phase; PR #1518 removes it from the new-run lifecycle (rows go straight from QUEUED to RUNNING). Historical rows and mid-flight runs can still land here."},
{label:"Running",color:"blue",value:"RUNNING",field:"RepositoryRecipeRunRunning",where:"Status column",warn:"After PR #1518 RUNNING is the first “active” state a row shows — it may be present while the same row's syncStatus is still PENDING. Row-derivation logic that assumes sync-then-run must handle run-then-sync too (repository-results.rows.ts, changeset-status.helpers.ts). Also verify resultsCount gating: a row in RUNNING with Sync=PENDING must not render a stale count.",d:"Recipe actively reading / transforming the LST. Now the first active state — may be present while syncStatus is still PENDING. Do not gate rendering on Sync=SYNCED."},
{label:"Finished",color:"green",value:"FINISHED",field:"RepositoryRecipeRunFinished / RepositoryBatchChange",where:"Status column",warn:"After PR #1518 FINISHED can arrive with syncStatus not yet SYNCED. RepoSynced and RepoRunFinished interleave in either order for the same repo.",d:"Recipe completed; results captured (the modified LST is then discarded). Can arrive with syncStatus still PENDING — event ordering is no longer guaranteed."},
{label:"Error",color:"red",value:"ERROR",field:"RepositoryRecipeRunError (+ RepositoryErrorReason)",where:"Status column · error tooltip",warn:"After PR #1518 errorMessage may be truncated at PIPE_BUF by the worker (cap on RepoRunFinished.errorMessage). Nothing to change in the UI — ErrorStatusPopover's copy-full-message affordance still works, just on a shorter payload.",d:"Recipe threw or worker failed. Error reasons: FAILED_LOAD_AST, FAILED_LOAD_RECIPE, RECIPE_ERROR, TIMEOUT. Message body is now capped server-side at PIPE_BUF; tooltip / popover copy still works."},
{label:"Canceled",color:"gold",value:"CANCELED",field:"RepositoryRecipeRunCanceled",where:"Status column",d:"Run canceled. Gold in the status column today."},
{label:"No LST",color:"amber",value:"NO_LST",field:"RepositoryRecipeRunNoLst",where:"Status column",d:"Short-circuit: no LST was available, the recipe cannot run. Pairs with Sync=SKIPPED."},
{label:"Unknown",color:"grey",value:"UNKNOWN",field:"ChangesetStatus",where:"Status column · fallback",d:"Fallback mapping — should not appear from the server. Kept so unmapped values fail safe rather than crashing the row renderer."}
],
[]
],
sugg_status:[
[],
[],
[],
[
{label:"Available",color:"success",value:"ready",field:"suggested",where:"grid",d:"Ready / done — a discrete per-repo state, so a chip. Design system: Success intent (green)."},
{label:"No LST",color:"warning",value:"evicted / gone / never built",field:"suggested",where:"grid",d:"Proposed: if the artifact is evicted, missing, or was never built, the row falls back to the same ‘No LST’ state — re-ingest to recover. Design system: Warning intent (gold). No separate ‘evicted’ badge. (No ‘Stale’ state either: we can't reliably detect drift — see gap note.)"}
],
[{label:"— collapsed into Run",color:"none",value:"no separate sync phase",field:"suggested",where:"run page",d:"Proposed: the platform stops surfacing Sync as its own phase. ‘Pending’ (LST being fetched) folds into Queued, ‘Synced’ becomes the moment a repo starts Running, and the terminal sync states (Failed / Skipped / Canceled) collapse into the run-phase terminal states. Some repos are already synced and start Running immediately. See the Run column for the unified status set."}],
[
{label:"Queued",color:"queued",value:"waiting (LST fetch or run slot)",field:"suggested",where:"status column",d:"Design system: Queued intent (violet, clock leading icon). Covers both ‘LST is being fetched & decrypted’ (old Pending) and ‘LST is ready, waiting for a run slot’ (old Queued). One waiting state; the user doesn't need to distinguish which kind of waiting it is."},
{label:"Running",color:"active",value:"executing recipe · indeterminate",field:"suggested",where:"status column",d:"Design system: Active intent (violet, spinning open-arc) — “Running · Processing · Syncing · In progress” all live under Active. Indeterminate motion carried by the loader-circle icon (no percentage; we can't compute reliable per-repo progress). Repos that were already synced enter Running immediately."},
{label:"Finished",color:"success",value:"FINISHED",field:"suggested",where:"status column",d:"Design system: Success intent (green) — discrete terminal state."},
{label:"Error",color:"error",value:"ERROR / sync failure",field:"suggested",where:"status column",d:"Design system: Error intent (red) — covers both run-time errors (old Error) and sync failures (old Failed). The user-facing outcome is the same: this repo did not produce results."},
{label:"Canceled",color:"canceled",value:"CANCELED",field:"suggested",where:"status column",d:"Design system: dedicated Canceled intent (warm grey) — distinct from Neutral so an inactive / draft chip and a canceled chip read differently. Consistent everywhere, covers cancellation at any point in the run."},
{label:"No LST",color:"warning",value:"NO_LST / skipped",field:"suggested",where:"status column",d:"Design system: Warning intent (gold) — no LST available for this repo, so the recipe can't run. Covers both the old Skipped (no artifact to fetch) and NoLst (short-circuit) cases."}
],
[]
]
};
const grid=document.getElementById("grid");
function el(tag,cls,html){const e=document.createElement(tag);if(cls)e.className=cls;if(html!=null)e.innerHTML=html;return e;}
/* DS Badge leading-icon per intent. Queued (clock) and Active (loader-circle) are mandatory
because they share the violet ramp — the icon is the only differentiator. Others
follow the StatusPage foundation table. */
const DS_ICON={
neutral:"circle-minus", grey:"circle-minus",
success:"circle-check", green:"circle-check",
queued:"clock",
active:"loader-circle",
info:"info", blue:"info",
warning:"triangle-alert", amber:"triangle-alert", gold:"triangle-alert",
error:"octagon-alert", red:"octagon-alert",
canceled:"circle-x",
none:null
};
function iconMarkup(color,legacy){
if(legacy) return '<span class="sw"></span>';
const name=DS_ICON[color];
if(!name) return '<span class="sw"></span>';
const spin=name==="loader-circle"?" spin":"";
return '<span class="badgeicon'+spin+'" aria-hidden="true"><svg><use href="#dsicon-'+name+'"/></svg></span>';
}
function fillChips(box,arr,i,legacy){
(arr||[]).forEach(st=>{
if(st.bar){
const pb=el("button","pbar click");
const ends=(st.from||st.to)?'<div class="pends"><span>'+(st.from||"")+'</span><span>→</span><span>'+(st.to||"")+'</span></div>':'';
pb.innerHTML=ends+'<div class="track"><div class="fill '+st.color+(st.striped?" striped":"")+'" style="width:'+st.pct+'%"></div></div><div class="plabel">'+st.label+'</div>';
pb.addEventListener("click",()=>openStatus(PHASES[i],st));
box.appendChild(pb);return;
}
const c=el("button","chip c-"+st.color+" click"+(legacy?" legacy":"")+(st.warn?" warned":""));
c.innerHTML=iconMarkup(st.color,legacy)+'<span class="chiplabel">'+st.label+'</span>'+(st.warn?' <span class="warn">⚠</span>':'');
c.addEventListener("click",()=>openStatus(PHASES[i],st));
box.appendChild(c);
});
}
// header
// zone band (CLI vs Platform UI)
grid.appendChild(el("div","zbcorner","Zone →"));
const zCli=el("div","zoneband zone-cli","CLI · admin / ingest");
zCli.style.gridColumn="span 3";grid.appendChild(zCli);
const zPlat=el("div","zoneband zone-platform","Platform UI · browse & run");
zPlat.style.gridColumn="span 4";grid.appendChild(zPlat);
// phase headers
grid.appendChild(el("div","gh corner","Phase →"));
PHASES.forEach((p,idx)=>{
const zone=idx<3?"cli":"platform";
const bound=idx===2?" zbound":"";
const parts=p.split(" · ");
grid.appendChild(el("div","gh "+zone+bound,`<span class="pnum">${parts[0]}</span> · ${parts.slice(1).join(" · ")}`));
});
let bandToggle=false;
ROWS.forEach(row=>{
if(row.type==="line"){
grid.appendChild(el("div","line-row"+(row.variant==="gap"?" gap":""),row.name));
return;
}
if(row.type==="toggleline"){
const tl=el("div","line-row toggle-line",'<span class="toggle">▸</span>'+row.name+'<span class="tlhint">(click to expand)</span>');
tl.addEventListener("click",()=>toggleGroup(row.id,tl));
grid.appendChild(tl);
return;
}
bandToggle=!bandToggle;
const grp=row.group?(" grp-"+row.group+" hiddenrow"):"";
const lbl=el("div","lane-label lane-"+row.id+grp,`${row.name}<small>${row.note||""}</small>`);
grid.appendChild(lbl);
if(row.id==="cli_status"){
STATUSES.cli_status.forEach((arr,i)=>{
const td=el("div","cell statuscell"+(bandToggle?"":" row-even")+(i===2?" zbound":"")+grp);
if(!bandToggle)td.style.background="color-mix(in srgb,var(--bg) 45%,var(--panel))";
if(!arr||!arr.length){td.appendChild(el("div","empty","—"));grid.appendChild(td);return;}
const sb=el("div","strs");
arr.forEach(st=>{
const s=el("button","strval click");
s.textContent=st.label;
s.addEventListener("click",()=>openStatus(PHASES[i],st));
sb.appendChild(s);
});
td.appendChild(sb);grid.appendChild(td);
});
return;
}
if(row.id==="plat_status"){
STATUSES.plat_status.forEach((today,i)=>{
const prop=STATUSES.sugg_status[i]||[];
const hasWarn=today&&today.some(s=>s.warn);
const td=el("div","cell statuscell splitcell"+(bandToggle?"":" row-even")+(i===2?" zbound":"")+(hasWarn?" cell-warn":""));
if(!bandToggle)td.style.background="color-mix(in srgb,var(--bg) 45%,var(--panel))";
const hT=el("div","half today"); hT.appendChild(el("span","hlabel","today"));
const tb=el("div","chips"); fillChips(tb,today,i,true); if(!today||!today.length)tb.appendChild(el("div","empty","—")); hT.appendChild(tb);
const hP=el("div","half prop"); hP.appendChild(el("span","hlabel","proposed"));
const pb2=el("div","chips"); fillChips(pb2,prop,i,false); if(!prop||!prop.length)pb2.appendChild(el("div","empty","—")); hP.appendChild(pb2);
td.appendChild(hT);td.appendChild(hP);grid.appendChild(td);
});
return;
}
C[row.id].forEach((cell,i)=>{
const td=el("div","cell"+(bandToggle?"":" row-even")+(i===2?" zbound":"")+grp);
if(!bandToggle)td.style.background="color-mix(in srgb,var(--bg) 45%,var(--panel))";
if(!cell){td.appendChild(el("div","empty","—"));grid.appendChild(td);return;}
const card=el("div","card cl-"+row.id);
card.tabIndex=0;card.setAttribute("role","button");
card.innerHTML=`<p class="t">${cell.t}</p><p class="s">${cell.s}</p>`;
const open=()=>openDrawer(row.id,row.name,PHASES[i],cell);
card.addEventListener("click",open);
card.addEventListener("keydown",e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();open();}});
td.appendChild(card);
grid.appendChild(td);
});
});
// drawer
function toggleGroup(id,tl){
const rows=document.querySelectorAll(".grp-"+id);
const willShow=rows.length&&rows[0].classList.contains("hiddenrow");
rows.forEach(e=>e.classList.toggle("hiddenrow"));
const tog=tl.querySelector(".toggle"); if(tog)tog.textContent=willShow?"▾":"▸";
const hint=tl.querySelector(".tlhint"); if(hint)hint.textContent=willShow?"(click to collapse)":"(click to expand)";
}
const drawer=document.getElementById("drawer"),backdrop=document.getElementById("backdrop");
const LANEMETA={evidence:getCss("--input"),user:getCss("--success"),frontstage:getCss("--info"),backstage:getCss("--process"),support:getCss("--neutral"),sugg_status:getCss("--success"),cli_status:getCss("--neutral"),plat_status:getCss("--neutral")};
function openDrawer(laneId,laneName,phase,cell){
const fg=LANEMETA[laneId]||getCss("--neutral");
const b=document.getElementById("dBadge");
b.textContent=laneName;b.style.color=fg;b.style.background="color-mix(in srgb,"+fg+" 16%, var(--panel))";
document.getElementById("dCtx").textContent=phase;
document.getElementById("dTitle").textContent=cell.t;
document.getElementById("dBody").innerHTML="<p>"+cell.d+"</p>";
drawer.classList.add("on");backdrop.classList.add("on");
}
function openStatus(phase,st){
const map={
grey:["Neutral",getCss("--neutral")], neutral:["Neutral",getCss("--neutral")],
green:["Success",getCss("--success")], success:["Success",getCss("--success")],
red:["Error",getCss("--fail")], error:["Error",getCss("--fail")],
amber:["Warning",getCss("--warning")], warning:["Warning",getCss("--warning")], gold:["Warning",getCss("--warning")],
blue:["Info",getCss("--info")], info:["Info",getCss("--info")],
queued:["Queued",getCss("--queued")],
active:["Active",getCss("--active")],
canceled:["Canceled",getCss("--canceled")],
none:["No badge",getCss("--neutral")]
};
const [cn,fg]=map[st.color]||["",getCss("--neutral")];
const b=document.getElementById("dBadge");
b.textContent=cn+" status";b.style.color=fg;b.style.background="color-mix(in srgb,"+fg+" 16%, var(--panel))";
document.getElementById("dCtx").textContent="Status • "+phase+" • "+st.where;
document.getElementById("dTitle").textContent=st.label;
let html="";
if(st.value)html+="<p><strong>Value:</strong> <code>"+st.value+"</code></p>";
if(st.field)html+="<p><strong>Source:</strong> <code>"+st.field+"</code></p>";
if(st.where)html+="<p><strong>Surfaces:</strong> "+st.where+"</p>";
if(st.d)html+="<p>"+st.d+"</p>";
if(st.warn)html+='<p style="margin-top:12px;padding:9px 11px;border-radius:8px;background:var(--process-bg);color:var(--process);font-size:13.5px;line-height:1.5"><strong>⚠ Unclear in the UI:</strong> '+st.warn+'</p>';
document.getElementById("dBody").innerHTML=html;
drawer.classList.add("on");backdrop.classList.add("on");
}
function closeDrawer(){drawer.classList.remove("on");backdrop.classList.remove("on");}
document.getElementById("dClose").addEventListener("click",closeDrawer);
backdrop.addEventListener("click",closeDrawer);
document.addEventListener("keydown",e=>{if(e.key==="Escape")closeDrawer();});
// legend
const legend=document.getElementById("legend");
const LEG=[
["Success · finished / synced / available",getCss("--success")],
["Queued · pending / awaiting slot",getCss("--queued")],
["Active · running / syncing / processing",getCss("--active")],
["Info · open / feature label",getCss("--info")],
["Warning · no LST / partially parsed",getCss("--warning")],
["Error · failed / timeout",getCss("--fail")],
["Neutral · unknown / draft / no changes",getCss("--neutral")],
["Canceled · canceled",getCss("--canceled")]
];
LEG.forEach(([label,fg])=>{
const it=el("div","it");
const d=el("span","dot");d.style.background=fg;
it.appendChild(d);it.appendChild(el("span",null,label));
legend.appendChild(it);
});
// ===== Responsive user-flow connectors =====
(function(){
const SVG_NS="http://www.w3.org/2000/svg";
const wrap=document.getElementById("flow2");
const svg=document.getElementById("flow2Overlay");
if(!wrap||!svg) return;
const defs=svg.querySelector("defs");
function bbox(id){
const e=wrap.querySelector(`[data-fid="${id}"]`);
if(!e) return null;
const r=e.getBoundingClientRect();
const w=wrap.getBoundingClientRect();
return {
left:r.left-w.left, right:r.right-w.left,
top:r.top-w.top, bottom:r.bottom-w.top,
cx:r.left-w.left+r.width/2, cy:r.top-w.top+r.height/2
};
}
function laneRect(sel){
const e=wrap.querySelector(sel);
if(!e) return null;
const r=e.getBoundingClientRect(), w=wrap.getBoundingClientRect();
return {left:r.left-w.left, right:r.right-w.left};
}
function mk(tag,attrs){
const n=document.createElementNS(SVG_NS,tag);
for(const k in attrs) n.setAttribute(k,attrs[k]);
return n;
}
function clear(){
[...svg.childNodes].forEach(n=>{ if(n!==defs) svg.removeChild(n); });
}
function line(x1,y1,x2,y2,opt){
svg.appendChild(mk("line",{x1,y1,x2,y2,
style:`stroke:${opt.color};stroke-width:${opt.w||1.5};${opt.dash?"stroke-dasharray:4 3;":""}`,
"marker-end":"url(#f2-arrow)"}));
}
function path(d,opt){
svg.appendChild(mk("path",{d,fill:"none",
style:`stroke:${opt.color};stroke-width:${opt.w||1.5};${opt.dash?"stroke-dasharray:6 4;":""}`,
"marker-end":"url(#f2-arrow)"}));
}
function label(x,y,text,color){
const t=mk("text",{x,y,style:`fill:${color};font-size:11px;font-family:inherit;`});
t.textContent=text;
svg.appendChild(t);
}
function draw(){
const w=wrap.getBoundingClientRect();
svg.setAttribute("viewBox",`0 0 ${w.width} ${w.height}`);
svg.setAttribute("width",w.width);
svg.setAttribute("height",w.height);
clear();
const INK="var(--ink2)", DIM="var(--ink3)";
const COL={
success:"var(--success)", info:"var(--info)", fail:"var(--fail)",
process:"var(--process)", neutral:"var(--neutral)", gold:"#c9a227"
};
const PAD=4;
// Horizontal spine — one continuous chain. The handoff link uses a dashed info-blue style.
function hline(fromId,toId,opt){
const a=bbox(fromId), b=bbox(toId);
if(!a||!b) return;
line(a.right+PAD,a.cy,b.left-PAD,b.cy,{color:opt&&opt.color||INK,w:opt&&opt.w||1.5,dash:opt&&opt.dash});
}
[["cli-start","cli-rebuild"],["cli-rebuild","cli-build"],["cli-build","cli-buildok"],
["cli-buildok","cli-publish"],["cli-publish","cli-artifact"]].forEach(([a,b])=>hline(a,b));
// handoff
hline("cli-artifact","plat-lst",{color:COL.info,w:1.6,dash:true});
[["plat-lst","plat-queued"],["plat-queued","plat-run"],
["plat-run","plat-result"],["plat-result","plat-end"]].forEach(([a,b])=>hline(a,b));
// Up branch: Rebuild? → Reuse pill, with the "no · reuse" loop continuing on to Publish above the spine.
(function(){
const rebuild=bbox("cli-rebuild"), reuse=bbox("cli-reuse"), publish=bbox("cli-publish");
if(!rebuild||!reuse||!publish) return;
// vertical link from Rebuild? top → Reuse bottom
line(rebuild.cx, rebuild.top-PAD, reuse.cx, reuse.bottom+PAD, {color:COL.success,w:1.4});
const noLbl=mk("text",{x:rebuild.cx+8,y:rebuild.top-6,style:`fill:${COL.success};font-size:10.5px;font-family:inherit;font-weight:600;`});
noLbl.textContent="no";
svg.appendChild(noLbl);
// arc above the spine to Publish
const arcY = Math.max(8, reuse.top - 14);
path(`M${reuse.cx} ${reuse.top-PAD} L${reuse.cx} ${arcY} L${publish.cx} ${arcY} L${publish.cx} ${publish.top-PAD}`,
{color:COL.success,w:1.4,dash:true});
const lbl=mk("text",{x:(reuse.cx+publish.cx)/2,y:arcY-4,"text-anchor":"middle",style:`fill:${COL.success};font-size:10.5px;font-family:inherit;`});
lbl.textContent="reuse existing LST";
svg.appendChild(lbl);
})();
// Down branch: node → aux-bottom destination. Label tucked to one side of the drop column.
function downBranch(fromId,toId,color,lbl){
const a=bbox(fromId), b=bbox(toId);
if(!a||!b) return;
const turnY=(a.bottom + b.top)/2;
path(`M${a.cx} ${a.bottom+PAD} L${a.cx} ${turnY} L${b.cx} ${turnY} L${b.cx} ${b.top-PAD}`,
{color,w:1.3,dash:true});
if(lbl){
const t=mk("text",{x:a.cx+8,y:a.bottom+12,style:`fill:${color};font-size:10.5px;font-family:inherit;font-weight:600;`});
t.textContent=lbl;
svg.appendChild(t);
}
}
downBranch("cli-buildok","cli-failed",COL.fail,"no");
downBranch("plat-lst","plat-nolst-1",COL.process,"no");
downBranch("plat-result","plat-error",COL.fail);
downBranch("plat-result","plat-canceled-2",COL.neutral);
}
// draw + observe
let raf=0;
function schedule(){ cancelAnimationFrame(raf); raf=requestAnimationFrame(draw); }
if("ResizeObserver" in window){
new ResizeObserver(schedule).observe(wrap);
}
window.addEventListener("resize",schedule);
// wait for fonts/layout
if(document.readyState==="complete") schedule();
else window.addEventListener("load",schedule);
schedule();
})();
</script>
</body>
</html>