changeledger 0.6.4 → 0.8.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.
@@ -19,6 +19,7 @@ import {
19
19
  normalizeRepoState,
20
20
  restoreViewerState,
21
21
  selectProject,
22
+ setDetailPresentation,
22
23
  setOwnerFilter,
23
24
  setRepo,
24
25
  setSortKey,
@@ -36,7 +37,7 @@ import { boardStatuses, isVisible, passesTombstones } from './state.js';
36
37
  import { html, render as litRender, nothing } from './templates.js';
37
38
  import {
38
39
  card,
39
- closeButton,
40
+ detailToolbar,
40
41
  sortIndicator,
41
42
  specBody,
42
43
  stageBlock,
@@ -50,6 +51,8 @@ export { cssIdent, esc, makeMermaidExpandable, safeHtml } from './security.js';
50
51
  export { boardStatuses, isVisible, passesTombstones } from './state.js';
51
52
  export {
52
53
  card,
54
+ detailPresentationControls,
55
+ detailToolbar,
53
56
  sortIndicator,
54
57
  stageBlock,
55
58
  statusSummary,
@@ -312,6 +315,73 @@ export async function runValidationSubmission({ root, request, onSuccess }) {
312
315
  return true;
313
316
  }
314
317
 
318
+ export function reopenPanel(status) {
319
+ if (status !== 'done') return nothing;
320
+ return html`<section class="validation-actions" aria-labelledby="reopen-title">
321
+ <div class="validation-copy">
322
+ <span class="validation-kicker">Lifecycle correction</span>
323
+ <h2 id="reopen-title">Reopen completed change</h2>
324
+ <p>Return this change to active work while preserving why its completion was reconsidered.</p>
325
+ </div>
326
+ <div class="reopen-controls">
327
+ <div class="rejection-field">
328
+ <label for="reopen-reason">Reason for reopening</label>
329
+ <input
330
+ id="reopen-reason"
331
+ data-reopen-reason
332
+ type="text"
333
+ placeholder="What requires more work?"
334
+ />
335
+ <p class="validation-error" role="alert" hidden></p>
336
+ </div>
337
+ <button type="button" class="button button-danger" data-reopen>Reopen change</button>
338
+ </div>
339
+ </section>`;
340
+ }
341
+
342
+ export function bindReopenAction({ root, request, onSuccess }) {
343
+ const button = root.querySelector('[data-reopen]');
344
+ if (!button) return;
345
+ button.onclick = async () => {
346
+ const input = root.querySelector('[data-reopen-reason]');
347
+ const reason = input?.value.trim();
348
+ if (!reason) {
349
+ showValidationError(root, 'A reopening reason is required.');
350
+ input?.focus();
351
+ return false;
352
+ }
353
+ return runValidationSubmission({
354
+ root,
355
+ request: () => request(reason),
356
+ onSuccess,
357
+ });
358
+ };
359
+ }
360
+
361
+ export function applyDetailPresentation(root = document) {
362
+ const overlay = root.querySelector('#overlay');
363
+ const detail = root.querySelector('#detail');
364
+ if (!overlay || !detail) return;
365
+ overlay.dataset.detailMode = state.detailMode;
366
+ detail.dataset.detailSize = state.detailSize;
367
+ }
368
+
369
+ export function bindDetailPresentation(root = document) {
370
+ root.querySelectorAll('[data-detail-setting]').forEach((button) => {
371
+ button.onclick = () => {
372
+ const setting = button.dataset.detailSetting;
373
+ setDetailPresentation(
374
+ setting === 'mode' ? button.dataset.detailValue : state.detailMode,
375
+ setting === 'size' ? button.dataset.detailValue : state.detailSize,
376
+ );
377
+ applyDetailPresentation(root);
378
+ root.querySelectorAll(`[data-detail-setting="${setting}"]`).forEach((option) => {
379
+ option.setAttribute('aria-pressed', String(option === button));
380
+ });
381
+ };
382
+ });
383
+ }
384
+
315
385
  async function submitValidation(id, status, reason) {
316
386
  const root = $('#detail');
317
387
  await runValidationSubmission({
@@ -325,6 +395,28 @@ async function submitValidation(id, status, reason) {
325
395
  });
326
396
  }
327
397
 
398
+ export function resetDetailScroll(
399
+ detail,
400
+ schedule = globalThis.requestAnimationFrame?.bind(globalThis),
401
+ ) {
402
+ const reset = () => {
403
+ detail.scrollTo?.({ top: 0, left: 0, behavior: 'instant' });
404
+ detail.scrollTop = 0;
405
+ };
406
+ reset();
407
+ schedule?.(reset);
408
+ }
409
+
410
+ export function scrollToStage(stage) {
411
+ stage.scrollIntoView({ behavior: 'auto', block: 'start' });
412
+ }
413
+
414
+ function renderOpenedDetail(content) {
415
+ const detail = $('#detail');
416
+ litRender(content, detail);
417
+ resetDetailScroll(detail);
418
+ }
419
+
328
420
  function openDetail(id) {
329
421
  const c = state.repo.changes.find((x) => String(x.id) === String(id));
330
422
  if (!c) return;
@@ -334,14 +426,11 @@ function openDetail(id) {
334
426
  ? html`<span class="pill ext" data-extdep=${d} style="cursor:pointer">depends on ${d}</span>`
335
427
  : html`<span class="pill" data-dep=${d} style="cursor:pointer">depends on #${d}</span>`;
336
428
  });
337
- const pipeline = c.stages.map(
338
- (s) => html`<span class="stage-chip" data-go=${`stage-${s.key}`}>${s.heading}</span>`,
339
- );
340
429
  const stages = c.stages.map((s) => stageBlock(c, s));
341
430
 
342
- litRender(
431
+ renderOpenedDetail(
343
432
  html`
344
- ${closeButton()}
433
+ ${detailToolbar(state.detailMode, state.detailSize, c.stages)}
345
434
  <h1>${c.title}</h1>
346
435
  <div class="detail-meta">
347
436
  <span class="pill">#${c.id}</span>
@@ -352,16 +441,18 @@ function openDetail(id) {
352
441
  ${deps}
353
442
  </div>
354
443
  ${c.status === 'in-validation' ? validationPanel() : nothing}
355
- <div class="pipeline">${pipeline}</div>
444
+ ${reopenPanel(c.status)}
356
445
  ${stages}
357
446
  <div id="git-section"></div>`,
358
- $('#detail'),
359
447
  );
360
448
 
361
449
  resetValidationState($('#detail'));
362
450
 
363
451
  const overlay = $('#overlay');
364
452
  overlay.classList.remove('hidden');
453
+ document.documentElement.classList.add('detail-open');
454
+ applyDetailPresentation();
455
+ bindDetailPresentation();
365
456
  $('#detail').querySelector('.close').onclick = closeDetail;
366
457
  const accept = $('#detail').querySelector('[data-validation="pass"]');
367
458
  if (accept) accept.onclick = () => submitValidation(c.id, 'done');
@@ -378,14 +469,22 @@ function openDetail(id) {
378
469
  submitValidation(c.id, 'in-progress', reason);
379
470
  };
380
471
  }
472
+ bindReopenAction({
473
+ root: $('#detail'),
474
+ request: (reason) => postStatus(state.currentProject, c.id, 'in-progress', reason),
475
+ onSuccess: async () => {
476
+ invalidateCache();
477
+ await load();
478
+ openDetail(c.id);
479
+ },
480
+ });
381
481
  overlay.onclick = (e) => {
382
482
  if (e.target === overlay) closeDetail();
383
483
  };
384
484
  $('#detail')
385
485
  .querySelectorAll('[data-go]')
386
486
  .forEach((el) => {
387
- el.onclick = () =>
388
- $(`#${el.dataset.go}`).scrollIntoView({ behavior: 'smooth', block: 'start' });
487
+ el.onclick = () => scrollToStage($(`#${el.dataset.go}`));
389
488
  });
390
489
  $('#detail')
391
490
  .querySelectorAll('[data-dep]')
@@ -441,6 +540,7 @@ async function loadGitRefs(id) {
441
540
 
442
541
  function closeDetail() {
443
542
  $('#overlay').classList.add('hidden');
543
+ document.documentElement.classList.remove('detail-open');
444
544
  }
445
545
 
446
546
  let diagramLightbox = null;
@@ -584,9 +684,9 @@ function renderSpecs() {
584
684
  }
585
685
 
586
686
  function openSpec(s) {
587
- litRender(
687
+ renderOpenedDetail(
588
688
  html`
589
- ${closeButton()}
689
+ ${detailToolbar(state.detailMode, state.detailSize)}
590
690
  <h1>${s.title}</h1>
591
691
  <div class="detail-meta">
592
692
  <span class="pill">spec</span>
@@ -594,10 +694,12 @@ function openSpec(s) {
594
694
  ${(s.tags || []).map((t) => html`<span class="pill">${t}</span>`)}
595
695
  </div>
596
696
  ${specBody(s.body)}`,
597
- $('#detail'),
598
697
  );
599
698
  const overlay = $('#overlay');
600
699
  overlay.classList.remove('hidden');
700
+ document.documentElement.classList.add('detail-open');
701
+ applyDetailPresentation();
702
+ bindDetailPresentation();
601
703
  const detail = $('#detail');
602
704
  detail.querySelector('.close').onclick = closeDetail;
603
705
  overlay.onclick = (e) => {
@@ -23,12 +23,34 @@
23
23
  --status-discarded: #9aa0aa;
24
24
  --status-muted: #8b929e;
25
25
  font-family: "Avenir Next", Avenir, ui-sans-serif, sans-serif;
26
+ color-scheme: dark;
26
27
  }
27
28
 
28
29
  * {
29
30
  box-sizing: border-box;
30
31
  }
31
32
 
33
+ html,
34
+ body {
35
+ scrollbar-width: thin;
36
+ scrollbar-color: #3a414e transparent;
37
+ }
38
+ html::-webkit-scrollbar,
39
+ body::-webkit-scrollbar {
40
+ width: 8px;
41
+ height: 8px;
42
+ }
43
+ html::-webkit-scrollbar-track,
44
+ body::-webkit-scrollbar-track {
45
+ background: transparent;
46
+ }
47
+ html::-webkit-scrollbar-thumb,
48
+ body::-webkit-scrollbar-thumb {
49
+ background: #3a414e;
50
+ border: 2px solid var(--bg);
51
+ border-radius: 999px;
52
+ }
53
+
32
54
  body {
33
55
  margin: 0;
34
56
  background: var(--bg);
@@ -527,16 +549,31 @@ body {
527
549
  padding: 16px;
528
550
  overflow-x: auto;
529
551
  align-items: flex-start;
552
+ }
553
+ .board,
554
+ .detail,
555
+ .detail pre,
556
+ .detail table {
530
557
  scrollbar-width: thin;
531
558
  scrollbar-color: #3a414e transparent;
532
559
  }
533
- .board::-webkit-scrollbar {
560
+ .board::-webkit-scrollbar,
561
+ .detail::-webkit-scrollbar,
562
+ .detail pre::-webkit-scrollbar,
563
+ .detail table::-webkit-scrollbar {
564
+ width: 8px;
534
565
  height: 8px;
535
566
  }
536
- .board::-webkit-scrollbar-track {
567
+ .board::-webkit-scrollbar-track,
568
+ .detail::-webkit-scrollbar-track,
569
+ .detail pre::-webkit-scrollbar-track,
570
+ .detail table::-webkit-scrollbar-track {
537
571
  background: transparent;
538
572
  }
539
- .board::-webkit-scrollbar-thumb {
573
+ .board::-webkit-scrollbar-thumb,
574
+ .detail::-webkit-scrollbar-thumb,
575
+ .detail pre::-webkit-scrollbar-thumb,
576
+ .detail table::-webkit-scrollbar-thumb {
540
577
  background: #3a414e;
541
578
  border: 2px solid var(--bg);
542
579
  border-radius: 999px;
@@ -545,10 +582,10 @@ body {
545
582
  background: var(--panel);
546
583
  border: 1px solid var(--line);
547
584
  border-radius: 12px;
548
- width: clamp(238px, calc((100vw - 116px) / 7), 320px);
549
- min-width: 238px;
550
- max-width: 320px;
551
- flex: 0 0 clamp(238px, calc((100vw - 116px) / 7), 320px);
585
+ width: clamp(190px, calc((100vw - 140px) / 6), 400px);
586
+ min-width: 190px;
587
+ max-width: 400px;
588
+ flex: 0 0 clamp(190px, calc((100vw - 140px) / 6), 400px);
552
589
  }
553
590
  .column-head {
554
591
  display: flex;
@@ -593,6 +630,8 @@ body {
593
630
  font-family: ui-monospace, monospace;
594
631
  color: var(--muted);
595
632
  font-size: 12px;
633
+ min-width: 0;
634
+ overflow-wrap: anywhere;
596
635
  }
597
636
  .type-tag {
598
637
  font-size: 10px;
@@ -625,6 +664,7 @@ body {
625
664
  .card-title {
626
665
  font-weight: 600;
627
666
  line-height: 1.3;
667
+ overflow-wrap: anywhere;
628
668
  }
629
669
  .progress {
630
670
  margin-top: 9px;
@@ -640,6 +680,7 @@ body {
640
680
  }
641
681
  .card-meta {
642
682
  display: flex;
683
+ flex-wrap: wrap;
643
684
  gap: 10px;
644
685
  margin-top: 7px;
645
686
  font-size: 11px;
@@ -689,6 +730,13 @@ body {
689
730
  gap: 10px;
690
731
  margin-top: 15px;
691
732
  }
733
+ .reopen-controls {
734
+ display: grid;
735
+ grid-template-columns: minmax(190px, 1fr) auto;
736
+ align-items: end;
737
+ gap: 10px;
738
+ margin-top: 15px;
739
+ }
692
740
  .rejection-field {
693
741
  display: flex;
694
742
  flex-direction: column;
@@ -750,6 +798,7 @@ body {
750
798
  }
751
799
  .owner {
752
800
  font-weight: 600;
801
+ overflow-wrap: anywhere;
753
802
  }
754
803
  .card.archived {
755
804
  opacity: 0.55;
@@ -847,21 +896,107 @@ body {
847
896
  }
848
897
 
849
898
  /* Overlay + detail */
899
+ .detail-open {
900
+ overflow: hidden;
901
+ }
850
902
  .overlay {
851
903
  position: fixed;
852
904
  inset: 0;
853
905
  background: rgba(0, 0, 0, 0.55);
854
906
  display: flex;
855
907
  justify-content: flex-end;
908
+ align-items: stretch;
856
909
  z-index: 20;
857
910
  }
858
911
  .detail {
859
- width: min(720px, 92vw);
912
+ --detail-width: 960px;
913
+ container-type: inline-size;
914
+ position: relative;
915
+ width: min(var(--detail-width), calc(100vw - 48px));
860
916
  height: 100%;
861
917
  background: var(--bg);
862
918
  border-left: 1px solid var(--line);
863
919
  overflow-y: auto;
920
+ overflow-anchor: none;
864
921
  padding: 22px 26px 60px;
922
+ box-shadow: -24px 0 70px rgba(0, 0, 0, 0.28);
923
+ }
924
+ .detail[data-detail-size="compact"] {
925
+ --detail-width: 720px;
926
+ }
927
+ .detail[data-detail-size="wide"] {
928
+ --detail-width: 960px;
929
+ }
930
+ .detail[data-detail-size="full"] {
931
+ --detail-width: 1280px;
932
+ }
933
+ .overlay[data-detail-mode="floating"] {
934
+ align-items: center;
935
+ justify-content: center;
936
+ padding: 28px;
937
+ }
938
+ .overlay[data-detail-mode="floating"] .detail {
939
+ width: min(var(--detail-width), calc(100vw - 56px));
940
+ height: min(92vh, calc(100vh - 56px));
941
+ border: 1px solid var(--line);
942
+ border-radius: 16px;
943
+ box-shadow: 0 28px 90px rgba(0, 0, 0, 0.48);
944
+ }
945
+ .detail-toolbar {
946
+ position: sticky;
947
+ top: -22px;
948
+ z-index: 3;
949
+ display: grid;
950
+ grid-template-columns: auto minmax(0, 1fr) auto;
951
+ align-items: center;
952
+ gap: 12px;
953
+ margin: -22px -26px 20px;
954
+ padding: 14px 26px 10px;
955
+ border-bottom: 1px solid color-mix(in srgb, var(--line) 72%, transparent);
956
+ background: color-mix(in srgb, var(--bg) 92%, transparent);
957
+ backdrop-filter: blur(14px);
958
+ }
959
+ .detail-presentation {
960
+ display: flex;
961
+ flex-wrap: wrap;
962
+ gap: 8px;
963
+ width: fit-content;
964
+ }
965
+ .detail-choice {
966
+ display: inline-flex;
967
+ padding: 3px;
968
+ border: 1px solid var(--line);
969
+ border-radius: 9px;
970
+ background: var(--panel);
971
+ }
972
+ .detail-option {
973
+ min-height: 28px;
974
+ padding: 4px 9px;
975
+ border: 0;
976
+ border-radius: 6px;
977
+ background: transparent;
978
+ color: var(--muted);
979
+ font: inherit;
980
+ font-size: 11px;
981
+ cursor: pointer;
982
+ }
983
+ .detail-option[aria-pressed="true"] {
984
+ background: var(--panel-2);
985
+ color: var(--text);
986
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 42%, var(--line));
987
+ }
988
+ .detail-option:focus-visible {
989
+ outline: 2px solid var(--accent);
990
+ outline-offset: 2px;
991
+ }
992
+ .detail pre {
993
+ max-width: 100%;
994
+ overflow-x: auto;
995
+ }
996
+ .detail table {
997
+ display: block;
998
+ max-width: 100%;
999
+ overflow-x: auto;
865
1000
  }
866
1001
  .detail h1 {
867
1002
  font-size: 22px;
@@ -909,19 +1044,27 @@ body {
909
1044
  outline-offset: 3px;
910
1045
  }
911
1046
  .close {
912
- position: absolute;
913
- top: 16px;
914
- right: 22px;
1047
+ justify-self: end;
1048
+ }
1049
+ .detail-toolbar-close {
1050
+ grid-column: 3;
915
1051
  }
916
1052
 
917
1053
  .pipeline {
918
1054
  display: flex;
919
- flex-wrap: wrap;
1055
+ min-width: 0;
1056
+ flex-wrap: nowrap;
920
1057
  gap: 6px;
921
- margin: 16px 0;
1058
+ margin: 0;
1059
+ padding: 3px 2px;
1060
+ overflow-x: auto;
1061
+ overscroll-behavior-x: contain;
1062
+ scrollbar-width: thin;
922
1063
  }
923
1064
  .stage-chip {
1065
+ flex: 0 0 auto;
924
1066
  font-size: 12px;
1067
+ font-family: inherit;
925
1068
  padding: 5px 11px;
926
1069
  border-radius: 999px;
927
1070
  background: var(--panel);
@@ -937,6 +1080,7 @@ body {
937
1080
  .stage {
938
1081
  border-top: 1px solid var(--line);
939
1082
  padding-top: 14px;
1083
+ scroll-margin-top: 72px;
940
1084
  margin-top: 18px;
941
1085
  }
942
1086
  .stage h2 {
@@ -1235,7 +1379,8 @@ body {
1235
1379
  width: 100%;
1236
1380
  flex: 0 0 auto;
1237
1381
  }
1238
- .validation-controls {
1382
+ .validation-controls,
1383
+ .reopen-controls {
1239
1384
  grid-template-columns: 1fr;
1240
1385
  align-items: stretch;
1241
1386
  }
@@ -1255,7 +1400,32 @@ body {
1255
1400
  }
1256
1401
  .detail {
1257
1402
  width: 100vw;
1403
+ max-width: none;
1404
+ height: 100vh;
1405
+ max-height: none;
1258
1406
  padding: 18px 16px 60px;
1407
+ border: 0;
1408
+ border-radius: 0;
1409
+ }
1410
+ .overlay[data-detail-mode] {
1411
+ align-items: stretch;
1412
+ justify-content: stretch;
1413
+ padding: 0;
1414
+ }
1415
+ .overlay[data-detail-mode] .detail {
1416
+ width: 100vw;
1417
+ height: 100vh;
1418
+ max-height: none;
1419
+ border: 0;
1420
+ border-radius: 0;
1421
+ }
1422
+ .detail-toolbar {
1423
+ top: -18px;
1424
+ margin: -18px -16px 16px;
1425
+ padding: 10px 16px 8px;
1426
+ }
1427
+ .detail-option {
1428
+ padding-inline: 7px;
1259
1429
  }
1260
1430
  .grid th,
1261
1431
  .grid td {
@@ -1263,6 +1433,27 @@ body {
1263
1433
  }
1264
1434
  }
1265
1435
 
1436
+ /* Detail toolbar stacks its stage navigation onto its own row whenever the
1437
+ detail panel itself lacks room, independent of the surrounding viewport —
1438
+ this also covers the "Compact" width preset, not just narrow screens. */
1439
+ @container (max-width: 680px) {
1440
+ .detail-toolbar {
1441
+ grid-template-columns: minmax(0, 1fr) auto;
1442
+ gap: 8px;
1443
+ }
1444
+ .detail-toolbar-close {
1445
+ grid-column: 2;
1446
+ grid-row: 1;
1447
+ }
1448
+ .pipeline {
1449
+ grid-column: 1 / -1;
1450
+ grid-row: 2;
1451
+ }
1452
+ .stage {
1453
+ scroll-margin-top: 126px;
1454
+ }
1455
+ }
1456
+
1266
1457
  /* config editor tabs and form (113924) */
1267
1458
  .config-section {
1268
1459
  border-bottom: 1px solid var(--line);
@@ -36,6 +36,58 @@ export function closeButton(label = 'Close detail', extraClass = '') {
36
36
  </button>`;
37
37
  }
38
38
 
39
+ export function detailPresentationControls(mode = 'side', size = 'wide') {
40
+ const options = (values, selected, attr) =>
41
+ values.map(
42
+ ([value, label]) => html`<button
43
+ type="button"
44
+ class="detail-option"
45
+ data-detail-setting=${attr}
46
+ data-detail-value=${value}
47
+ aria-pressed=${String(selected === value)}
48
+ >${label}</button>`,
49
+ );
50
+ return html`<div class="detail-presentation" aria-label="Detail presentation">
51
+ <div class="detail-choice" role="group" aria-label="Layout">
52
+ ${options(
53
+ [
54
+ ['side', 'Side panel'],
55
+ ['floating', 'Floating modal'],
56
+ ],
57
+ mode,
58
+ 'mode',
59
+ )}
60
+ </div>
61
+ <div class="detail-choice" role="group" aria-label="Width">
62
+ ${options(
63
+ [
64
+ ['compact', 'Compact'],
65
+ ['wide', 'Wide'],
66
+ ['full', 'Full'],
67
+ ],
68
+ size,
69
+ 'size',
70
+ )}
71
+ </div>
72
+ </div>`;
73
+ }
74
+
75
+ export function detailToolbar(mode = 'side', size = 'wide', stages = []) {
76
+ const navigation = stages.length
77
+ ? html`<nav class="pipeline" aria-label="Change sections">
78
+ ${stages.map(
79
+ (stage) =>
80
+ html`<button type="button" class="stage-chip" data-go=${`stage-${stage.key}`}>${stage.heading}</button>`,
81
+ )}
82
+ </nav>`
83
+ : nothing;
84
+ return html`<div class="detail-toolbar" aria-label="Detail tools">
85
+ ${detailPresentationControls(mode, size)}
86
+ ${navigation}
87
+ ${closeButton('Close detail', 'detail-toolbar-close')}
88
+ </div>`;
89
+ }
90
+
39
91
  export function validationPanel() {
40
92
  return html`<section class="validation-actions" aria-labelledby="validation-title">
41
93
  <div class="validation-copy">
@@ -39,6 +39,12 @@ not necessarily that a spec was created.
39
39
  The graduation link remains derivable from the Log marker `graduado a spec`,
40
40
  which carries the spec link, rather than from the boolean flag.
41
41
 
42
+ After `--into` or `--skip`, create one final closure commit that coalesces any
43
+ pending `in-review → in-validation → done` ledger updates with the graduation
44
+ decision and durable spec edit. Do not create separate commits whose only
45
+ content is one of those transitions. If no lifecycle state is pending, the
46
+ graduation or skip itself remains the meaningful closure evidence.
47
+
42
48
  Operational inspection and visibility:
43
49
 
44
50
  - `changeledger list [--status S] [--type T] [--json]`
@@ -47,4 +53,4 @@ Operational inspection and visibility:
47
53
 
48
54
  Use Mermaid where it communicates persistent relationships better than prose.
49
55
  After closure, share a brief retrospective. New work needs a newly authorized
50
- change; `done` never reopens.
56
+ change; graduated, skipped, archived or released work never reopens.