beadcyte 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/LICENSE +661 -0
- package/README.md +386 -0
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/bin/beadcyte.mjs +60 -0
- package/package.json +77 -0
- package/src/changelog-cli.mjs +105 -0
- package/src/changelog.mjs +196 -0
- package/src/cli.mjs +577 -0
- package/src/estimator.mjs +314 -0
- package/src/format.mjs +22 -0
- package/src/history-walk.mjs +170 -0
- package/src/index.mjs +5 -0
- package/src/mutate.mjs +193 -0
- package/src/projects.mjs +120 -0
- package/src/provenance.mjs +75 -0
- package/src/review-hours.mjs +117 -0
- package/src/roster-path.mjs +24 -0
- package/src/scheduler.mjs +424 -0
- package/src/serve.mjs +411 -0
- package/src/server-state.mjs +105 -0
- package/src/ship.mjs +178 -0
- package/src/stage-waits.mjs +69 -0
- package/src/start.mjs +111 -0
- package/src/stop.mjs +66 -0
- package/src/velocity.mjs +209 -0
- package/src/web/App.vue +691 -0
- package/src/web/app.css +54 -0
- package/src/web/assets/favicon.svg +12 -0
- package/src/web/avatar.ts +53 -0
- package/src/web/bead-detail.ts +240 -0
- package/src/web/changelog-view.ts +41 -0
- package/src/web/components/BeadDrawer.vue +1883 -0
- package/src/web/components/BeadSubGraph.vue +326 -0
- package/src/web/components/BeadSubGraphOverlay.vue +192 -0
- package/src/web/components/BeadTooltip.vue +516 -0
- package/src/web/components/BeadcyteMark.vue +64 -0
- package/src/web/components/BeadsGantt.vue +2125 -0
- package/src/web/components/BeadsGrid.vue +468 -0
- package/src/web/components/BeadsIncytes.vue +567 -0
- package/src/web/components/BeadsMine.vue +325 -0
- package/src/web/components/BeadsTable.vue +335 -0
- package/src/web/components/ChangelogOverlay.vue +198 -0
- package/src/web/components/ContextMenu.vue +386 -0
- package/src/web/components/ControlsPanel.vue +476 -0
- package/src/web/components/CostTrend.vue +206 -0
- package/src/web/components/FilterPopover.vue +245 -0
- package/src/web/components/GroupProgress.vue +274 -0
- package/src/web/components/LoadMeter.vue +144 -0
- package/src/web/components/MineRow.vue +28 -0
- package/src/web/components/OptionsMenu.vue +825 -0
- package/src/web/components/PriorityChip.vue +105 -0
- package/src/web/components/ScoreStrip.vue +131 -0
- package/src/web/components/SearchPalette.vue +210 -0
- package/src/web/components/ShipTrend.vue +510 -0
- package/src/web/components/ShortcutsOverlay.vue +164 -0
- package/src/web/components/Term.vue +177 -0
- package/src/web/components/Toast.vue +50 -0
- package/src/web/components/TriageMeters.vue +426 -0
- package/src/web/components/TypeChip.vue +96 -0
- package/src/web/components/Walkthrough.vue +209 -0
- package/src/web/components/WhatIfPanel.vue +206 -0
- package/src/web/components/WipBullets.vue +191 -0
- package/src/web/components/filter-option.ts +9 -0
- package/src/web/composables/url-codec.ts +136 -0
- package/src/web/composables/useBeadTooltip.ts +148 -0
- package/src/web/composables/useKeyboard.ts +97 -0
- package/src/web/composables/useLiveRefresh.ts +69 -0
- package/src/web/composables/useTheme.ts +125 -0
- package/src/web/composables/useUrlState.ts +208 -0
- package/src/web/controls-scope.ts +83 -0
- package/src/web/cost.ts +251 -0
- package/src/web/dep-headings.ts +62 -0
- package/src/web/economics.ts +440 -0
- package/src/web/env.d.ts +85 -0
- package/src/web/frontier.ts +208 -0
- package/src/web/gantt-viewport.ts +99 -0
- package/src/web/highlights.ts +124 -0
- package/src/web/index.html +46 -0
- package/src/web/insights.ts +107 -0
- package/src/web/keybindings.ts +200 -0
- package/src/web/load-meter.ts +72 -0
- package/src/web/main.ts +20 -0
- package/src/web/markdown.ts +14 -0
- package/src/web/mine.ts +137 -0
- package/src/web/mutations.ts +21 -0
- package/src/web/person.ts +102 -0
- package/src/web/projects-text.ts +15 -0
- package/src/web/projects.ts +188 -0
- package/src/web/refresh.ts +47 -0
- package/src/web/search.ts +50 -0
- package/src/web/shortcuts.ts +113 -0
- package/src/web/status-filter.ts +48 -0
- package/src/web/store.ts +1378 -0
- package/src/web/style-audit.mjs +346 -0
- package/src/web/styles-alt.css +111 -0
- package/src/web/styles-ported.css +270 -0
- package/src/web/subgraph.ts +362 -0
- package/src/web/table.ts +201 -0
- package/src/web/theme.ts +88 -0
- package/src/web/tokens.css +168 -0
- package/src/web/triage.ts +914 -0
- package/src/web/view-model.ts +717 -0
- package/src/web/walkthrough.ts +133 -0
- package/src/web/watchlist.ts +47 -0
- package/src/web/whatif.ts +291 -0
- package/src/web/window.ts +73 -0
- package/src/web/wip.ts +83 -0
package/src/web/app.css
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Loaded via <link> in index.html. Component-scoped rules live in App.vue's
|
|
2
|
+
<style>. Global reset / tokens live here so future components inherit them
|
|
3
|
+
without importing anything. */
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
html,
|
|
10
|
+
body {
|
|
11
|
+
height: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ── Icons (@lucide/vue) ────────────────────────────────────────────────
|
|
15
|
+
Every lucide component renders a full <svg> carrying its own width/height
|
|
16
|
+
attributes, stroke="currentColor" and fill="none". CSS beats presentation
|
|
17
|
+
attributes in the cascade, so sizing is set here rather than passed as a
|
|
18
|
+
:size prop at each call site — one place to retune, and a named style
|
|
19
|
+
only has to touch tokens.
|
|
20
|
+
|
|
21
|
+
Colour needs no rule at all: stroke="currentColor" means an icon takes the
|
|
22
|
+
`color` of whatever it sits in.
|
|
23
|
+
|
|
24
|
+
Sizing is in em so an icon tracks the type size of its context: a chip's
|
|
25
|
+
icon shrinks with the chip. Call sites that need a different ratio set it
|
|
26
|
+
on their own class (.prio-icon, .type-icon), not with a pixel prop.
|
|
27
|
+
|
|
28
|
+
This works because a top-level <svg> in HTML takes its geometry from CSS.
|
|
29
|
+
A NESTED <svg> — which is how the same components are used inside
|
|
30
|
+
BeadsGantt's chart, positioned with x/y attributes — does NOT: it keeps
|
|
31
|
+
its width/height attributes and ignores CSS. In there, size is passed as
|
|
32
|
+
:size at the call site instead. See .gantt-icon in that component. */
|
|
33
|
+
|
|
34
|
+
.icon {
|
|
35
|
+
width: 1.15em;
|
|
36
|
+
height: 1.15em;
|
|
37
|
+
stroke-width: 2;
|
|
38
|
+
flex: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Visually hidden but still announced. Global rather than in a component:
|
|
42
|
+
OptionsMenu's form labels use it, and it previously lived in App.vue's
|
|
43
|
+
stylesheet — a cross-component dependency that only worked because that
|
|
44
|
+
stylesheet isn't scoped. */
|
|
45
|
+
.sr-only {
|
|
46
|
+
position: absolute;
|
|
47
|
+
width: 1px;
|
|
48
|
+
height: 1px;
|
|
49
|
+
margin: -1px;
|
|
50
|
+
padding: 0;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
clip-path: inset(50%);
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 265 210" fill-rule="evenodd">
|
|
2
|
+
<!-- An SVG favicon can carry its own theme response, which a PNG cannot:
|
|
3
|
+
browser tab strips are dark in dark mode and a black glyph vanishes
|
|
4
|
+
there. Values are the resolved --c-text ends of the token ladder. -->
|
|
5
|
+
<style>
|
|
6
|
+
path { fill: #1a1f29; }
|
|
7
|
+
@media (prefers-color-scheme: dark) {
|
|
8
|
+
path { fill: #e5e7eb; }
|
|
9
|
+
}
|
|
10
|
+
</style>
|
|
11
|
+
<path d="M 179.4 3.4 C 175.4 5.2, 171.2 10.3, 170.4 14.5 C 169.6 18.7, 171.5 24.7, 174.6 27.5 L 176.9 29.7 175 36.6 C 174 40.4, 172.6 45.3, 172.1 47.6 C 171.1 51.2, 170.5 51.8, 167.1 52.9 C 159.4 55.1, 154.5 64.1, 156.9 71.2 C 157.9 74.2, 157.8 74.7, 155.1 77.4 L 152.2 80.3 154.3 83.2 C 158.4 88.8, 156.2 97.4, 150 100 C 143.1 102.9, 136.1 98.4, 135.3 90.6 C 134.8 86.4, 135 85.8, 138.3 82.5 C 141.2 79.6, 142.5 79, 145.9 79 C 148.2 79, 150 78.6, 150 78.1 C 150 77.6, 147.9 76.2, 145.2 74.9 C 141.5 73, 138.9 72.5, 133 72.5 C 119.9 72.5, 108.8 79.4, 103.8 90.8 C 100.9 97.4, 100.3 107.5, 102.4 113.7 L 104.1 118.5 101 121.3 C 98.1 123.9, 97.8 124, 95 122.5 C 89.1 119.5, 82.1 121, 77 126.5 C 74.8 128.8, 74.5 130.1, 74.5 136.1 L 74.4 143.1 66.5 150.1 C 59.9 155.8, 58.1 156.9, 56 156.4 C 46.6 154.1, 42 155.1, 37.1 160.4 C 33.9 163.8, 33.2 171.2, 35.7 176 C 38.1 180.7, 45.1 184.3, 50.3 183.7 C 58.4 182.6, 63.2 176.7, 62.8 168.1 L 62.6 162.6 71.2 155.2 C 79 148.6, 80.1 148, 82.1 149 C 83.4 149.7, 86.7 150, 90.1 149.8 C 94.9 149.5, 96.6 148.8, 99 146.5 C 102.5 143.2, 104.4 137.2, 103.5 132.8 C 102.9 130.4, 103.3 129.3, 105.5 127 L 108.3 124.1 112.7 128 C 118.7 133.2, 125.4 135.4, 133.9 134.8 C 142.6 134.1, 150 130.8, 155.1 125.3 C 161.5 118.4, 163.5 113, 163.5 103 C 163.5 95.5, 163.2 93.9, 160.7 89.6 L 157.9 84.6 160.3 82.3 C 162.4 80.1, 163 80, 167.2 80.9 C 176.8 83.1, 184.8 77.7, 186 68.2 L 186.5 63.9 199.2 57.3 L 211.9 50.8 216.5 53 C 226.8 58, 238 51.5, 238 40.5 C 238 29, 227.2 23, 215.4 28 C 212.4 29.3, 211.9 29.1, 206 25.4 C 199.9 21.5, 199.8 21.4, 199.6 16.8 C 199.2 6.6, 188.3 -0.6, 179.4 3.4 M 186.4 10.5 C 184.9 14.3, 189.3 17.7, 191.6 14.5 C 192.5 13.2, 192.6 12.2, 191.9 10.9 C 190.7 8.6, 187.2 8.4, 186.4 10.5 M 120.5 24.7 C 106.3 26.7, 86 35.2, 70.1 45.8 C 61.9 51.3, 61.1 51.6, 58.9 50.4 C 53.8 47.6, 45.7 49.4, 41.1 54.4 C 38.3 57.3, 37 66.3, 38.8 69.7 C 39.7 71.4, 39.5 72.1, 37.3 74.1 C 35.9 75.4, 32.3 79.4, 29.3 83 L 23.9 89.5 18 89.5 C 11.7 89.5, 8.5 91.1, 4.6 96.3 C 1.9 100, 1.8 107.9, 4.6 111.7 C 7.8 116.1, 14.7 119.2, 19.6 118.4 C 23.7 117.8, 23.7 117.8, 29 124.4 C 34.3 131.2, 51.9 147.8, 56 150 C 58 151.1, 58.6 150.9, 60.9 148.6 L 63.5 146 61.2 144.4 C 50.1 136.6, 28.4 113.8, 29.6 111.1 C 32.1 105.3, 32.4 102, 30.7 98.5 L 29 95 36.7 86.2 C 40.9 81.4, 44.4 77.4, 44.5 77.2 C 44.6 77.1, 45.6 77.3, 46.6 77.7 C 48.2 78.4, 48.5 79.7, 48.6 86.5 C 48.7 93.3, 48.5 94.7, 46.9 95.8 C 43.3 98.3, 41.8 102.7, 43 106.5 C 45.5 114.1, 53.7 116.4, 59.2 111 C 61.7 108.5, 62.9 108, 67.1 108 C 71.3 108, 72 108.3, 72 110 C 72 111, 72.5 114.1, 73.1 116.8 L 74.1 121.7 78.8 119.4 C 81.4 118.1, 84.6 116.8, 86 116.6 L 88.6 116.1 87.6 109 C 84.6 87.6, 98.4 66.6, 119.7 60 C 127.5 57.6, 134.4 57.5, 142.5 59.6 C 145.8 60.4, 149.5 61.4, 150.6 61.6 C 152.3 62.1, 152.9 61.6, 153.4 59.6 C 153.7 58.2, 155.4 55.4, 157.1 53.5 C 158.9 51.5, 160.1 49.8, 159.9 49.6 C 158.1 48.3, 148.4 45, 143.6 44.1 C 129.9 41.6, 118.9 43.1, 106 49.2 C 89.2 57.2, 75.9 74.7, 72.9 92.5 L 71.6 100 66.9 100 C 62.9 100, 61.6 99.5, 59.1 96.9 C 56.2 94, 56 93.4, 56 86.2 C 56.1 78.7, 56.1 78.5, 59.3 76.5 C 64.7 73.1, 67.2 68.3, 66.8 62.5 L 66.4 57.4 76.1 51.2 C 106.6 31.6, 132.2 27, 159.8 36.2 C 164.9 37.9, 169.3 39.1, 169.5 38.8 C 169.7 38.6, 170.1 36.9, 170.2 35.1 C 170.5 31.9, 170.3 31.7, 165 29.8 C 151 24.9, 133.6 22.9, 120.5 24.7 M 189 30.5 L 183.6 32.8 180.9 43.2 L 178.2 53.5 180.3 55.2 C 181.5 56.2, 182.7 56.9, 183 56.9 C 183.3 56.9, 189.1 54.1, 196 50.7 L 208.5 44.5 208.8 40 L 209.1 35.5 202.7 31.7 C 199.2 29.7, 195.9 28.1, 195.4 28.1 C 194.9 28.2, 192 29.3, 189 30.5 M 226.2 32.7 C 223.6 33.7, 225.6 39, 228.7 39 C 229.4 39, 230.4 38.1, 231 37.1 C 231.7 35.6, 231.6 34.8, 230.4 33.6 C 228.8 31.9, 228.3 31.8, 226.2 32.7 M 53.7 57.4 C 53.4 58.2, 53.9 59.9, 54.9 61.1 C 57.3 64.1, 60.3 63, 59.8 59.3 C 59.6 57.1, 58.9 56.4, 56.9 56.2 C 55.2 56, 54 56.5, 53.7 57.4 M 204.8 58.8 C 203.2 59.8, 202 60.8, 202 61.1 C 202 61.4, 204.1 63.2, 206.7 65.1 C 211.9 68.8, 224 80.7, 231.4 89.3 C 235.7 94.3, 236 95, 234.9 97.4 C 233.8 99.7, 233.1 100, 228.2 100 C 223.6 100, 222.3 99.5, 219.3 97 C 214.9 93.1, 211.1 93.1, 206.7 97 C 203.4 99.9, 203.1 99.9, 190.9 100.2 L 178.5 100.5 177.9 108.2 C 176.6 123.6, 165.9 138.5, 151.3 145.2 C 139.3 150.7, 123.1 150.2, 110.8 143.9 L 107 141.9 106 144.6 C 105.4 146.1, 103.3 148.7, 101.4 150.5 L 97.9 153.8 103.2 156.8 C 122.6 167.8, 144.9 167.1, 165.2 154.9 L 169 152.6 171.9 155.4 C 173.8 157.3, 174.8 159.2, 174.8 160.9 C 174.7 162.4, 175.1 164.4, 175.6 165.3 C 176.1 166.2, 185.3 175.8, 195.9 186.5 C 216.5 207.2, 218.7 208.7, 225.3 206.9 C 229.4 205.8, 233.7 201.4, 234.9 197.2 C 236.9 190.3, 235.2 187.9, 214.1 166.8 C 196 148.6, 194.1 147, 190.9 147 C 188.3 147, 186.6 146.2, 184.3 143.8 L 181.2 140.5 185.1 134 C 189.4 126.9, 193 116.7, 193 111.4 L 193 108 198.4 108 C 202.6 108, 204.2 108.4, 205.2 109.8 C 208.8 115, 214.6 115.2, 219.9 110.5 C 223.2 107.5, 224.4 107, 228.7 107 C 233.1 107, 233.7 107.2, 234.6 109.9 C 235.6 112.4, 235.4 113.2, 233.2 116.1 C 229.2 121.4, 219.2 131.5, 210.3 139.2 L 202.1 146.2 204.5 148.8 C 206.7 151.2, 207.1 151.3, 209.5 150 C 213.3 148, 231.7 130.3, 237 123.6 C 241.4 118.1, 241.6 118, 245.5 118.6 C 254.2 119.8, 263 112.5, 263 104 C 263 95.6, 254.7 88.3, 246.3 89.4 C 242.2 89.9, 242.1 89.9, 235.6 82.4 C 226.5 72, 209.6 56.6, 207.8 57.1 C 207.7 57.1, 206.3 57.9, 204.8 58.8 M 172.6 60.5 C 171.9 62.5, 173.6 66, 175.3 66 C 177.7 66, 179.2 64.2, 178.8 61.9 C 178.4 59, 173.6 57.9, 172.6 60.5 M 185.4 80.2 C 184.3 81.3, 181.6 83.1, 179.2 84.2 C 176.9 85.2, 175 86.2, 175 86.3 C 175 86.4, 175.6 88.5, 176.4 91 L 177.7 95.5 185.1 95.1 C 189.2 94.9, 192.7 94.6, 192.8 94.5 C 193.4 94, 188.8 79, 188 78.5 C 187.6 78.2, 186.4 79, 185.4 80.2 M 19.1 97.3 C 16.6 100.4, 21.1 104.4, 23.9 101.5 C 25.1 100.3, 25.2 99.6, 24.4 98.2 C 22.9 96, 20.6 95.6, 19.1 97.3 M 250.7 96.6 C 249.4 97.9, 250 101.8, 251.5 102.4 C 254.2 103.4, 256.1 101.9, 255.8 99.1 C 255.5 96.5, 252.3 95, 250.7 96.6 M 90.4 130.5 C 90 131.4, 90.4 132.8, 91.3 133.7 C 93.4 135.8, 96.2 134.6, 95.8 131.8 C 95.4 129, 91.3 128, 90.4 130.5 M 73.2 159.3 C 71.1 161.5, 71.1 161.7, 72.7 163.1 C 80.5 169.6, 96.6 177.2, 109.8 180.7 C 121.7 183.8, 138.9 184.2, 151.5 181.5 C 159.6 179.8, 174 174.4, 174 173.1 C 174 172.7, 173 171.4, 171.7 170.2 L 169.4 168 164.3 170 C 153.9 173.9, 144.9 175.5, 132 175.5 C 115.5 175.5, 108.4 173.6, 90 164.3 C 82 160.3, 75.5 157, 75.4 157 C 75.4 157, 74.4 158, 73.2 159.3 M 50 163.9 C 50 166.3, 52.5 168.4, 54.5 167.6 C 55.3 167.3, 56 165.9, 56 164.5 C 56 162.4, 55.5 162, 53 162 C 50.7 162, 50 162.5, 50 163.9" />
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// avatar.ts — deterministic initials + colour for an assignee handle.
|
|
2
|
+
//
|
|
3
|
+
// Pure so the "same handle always looks the same" guarantee is testable
|
|
4
|
+
// without rendering a card.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Up to two initials for a handle. Splits on the separators that show up in
|
|
8
|
+
* real handles (space, dot, dash, underscore) and falls back to the first
|
|
9
|
+
* two characters when there's nothing to split on — "tstout" reads better as
|
|
10
|
+
* TS than as T.
|
|
11
|
+
*/
|
|
12
|
+
export function initialsOf(handle: string): string {
|
|
13
|
+
const cleaned = handle.trim();
|
|
14
|
+
if (!cleaned) return "?";
|
|
15
|
+
const parts = cleaned.split(/[\s._-]+/).filter(Boolean);
|
|
16
|
+
if (parts.length >= 2) {
|
|
17
|
+
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
return cleaned.slice(0, 2).toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stable hue for a handle: the same person is the same colour in every
|
|
24
|
+
* session and every view, without a colour table to maintain. Saturation and
|
|
25
|
+
* lightness are fixed so no handle can land on something unreadable — only
|
|
26
|
+
* the hue varies.
|
|
27
|
+
*/
|
|
28
|
+
export function hueFor(handle: string): number {
|
|
29
|
+
let h = 0;
|
|
30
|
+
for (let i = 0; i < handle.length; i++) {
|
|
31
|
+
// djb2-ish; |0 keeps it in int32 so the result can't drift with length.
|
|
32
|
+
h = (h * 33 + handle.charCodeAt(i)) | 0;
|
|
33
|
+
}
|
|
34
|
+
return Math.abs(h) % 360;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Chip background/foreground pair for a handle.
|
|
39
|
+
*
|
|
40
|
+
* The hue is per-person and computed, so this can't be a static token — but
|
|
41
|
+
* it still has to follow the theme, hence a light-dark() pair per value. On
|
|
42
|
+
* a light ground the chip fills pale and inks dark; on a dark ground it
|
|
43
|
+
* inverts. Authored in OKLCH for the same reason the tokens are: only
|
|
44
|
+
* lightness moves between the two, and in OKLCH that stays an even step
|
|
45
|
+
* whatever the hue.
|
|
46
|
+
*/
|
|
47
|
+
export function colorsFor(handle: string): { bg: string; fg: string } {
|
|
48
|
+
const h = hueFor(handle);
|
|
49
|
+
return {
|
|
50
|
+
bg: `light-dark(oklch(90% 0.055 ${h}), oklch(32% 0.070 ${h}))`,
|
|
51
|
+
fg: `light-dark(oklch(38% 0.110 ${h}), oklch(86% 0.090 ${h}))`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// bead-detail.ts — pure helpers for building tooltip content + external URLs.
|
|
2
|
+
//
|
|
3
|
+
// Separated from BeadsGantt.vue so we can unit-test the mapping without
|
|
4
|
+
// spinning up a component. All inputs are plain data: a bead, its schedule
|
|
5
|
+
// entry (if any), and an estimator result (if any); output is a flat object
|
|
6
|
+
// the tooltip component renders directly.
|
|
7
|
+
|
|
8
|
+
import { describeEstimateSource } from "../estimator.mjs";
|
|
9
|
+
import type { Bead } from "./store";
|
|
10
|
+
import type { ScheduleEntry } from "./view-model";
|
|
11
|
+
import { formatDays } from "../format.mjs";
|
|
12
|
+
import { shipEvidence, shipRecordProblem } from "../ship.mjs";
|
|
13
|
+
import type { Provenance } from "../provenance.mjs";
|
|
14
|
+
|
|
15
|
+
export interface EstimateResult {
|
|
16
|
+
days: number;
|
|
17
|
+
p90: number | null;
|
|
18
|
+
source: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A scheduled window, as dates rather than day offsets.
|
|
23
|
+
*
|
|
24
|
+
* The scheduler works in whole days from `startOfDay(now)` — negative for
|
|
25
|
+
* work already underway — and those raw offsets used to go straight to the
|
|
26
|
+
* tooltip, which rendered "+7d -> +8d". Nobody plans against an offset
|
|
27
|
+
* whose origin isn't stated, so the conversion happens here, once, where it
|
|
28
|
+
* can be tested.
|
|
29
|
+
*/
|
|
30
|
+
export interface ScheduledWindow {
|
|
31
|
+
/** Days from today, as the scheduler produced them. Kept for the chart. */
|
|
32
|
+
startDay: number;
|
|
33
|
+
endDay: number;
|
|
34
|
+
startDate: Date;
|
|
35
|
+
/**
|
|
36
|
+
* Projected finish. Only a projection when `beyondHorizon` is false — see
|
|
37
|
+
* below.
|
|
38
|
+
*/
|
|
39
|
+
endDate: Date;
|
|
40
|
+
/** Whole days from start to finish, minimum 1 (a same-day bead is a day). */
|
|
41
|
+
spanDays: number;
|
|
42
|
+
/** The scheduler placed this start in the past, from `started_at`. */
|
|
43
|
+
inFlight: boolean;
|
|
44
|
+
/** The work inside the span, in days (bp-m0g); null from an older entry. */
|
|
45
|
+
workDays: number | null;
|
|
46
|
+
/** Waiting inside the span: before the work (unclaimed) and after (review). */
|
|
47
|
+
waitBeforeDays: number;
|
|
48
|
+
waitAfterDays: number;
|
|
49
|
+
/**
|
|
50
|
+
* The finish was CLAMPED to the scheduling horizon rather than projected.
|
|
51
|
+
* scheduleGreedy caps `end_day` at the horizon for anything still running
|
|
52
|
+
* when it arrives, so the value means "no sooner than this" — and because
|
|
53
|
+
* the horizon follows the window preset, the same bead reports a
|
|
54
|
+
* different date at ±30d and ±180d. Presenting a clamped value as a firm
|
|
55
|
+
* date is the one thing this type exists to prevent.
|
|
56
|
+
*
|
|
57
|
+
* The test is `>=`, so a bead that genuinely finishes exactly ON the
|
|
58
|
+
* horizon also reads as a bound. That is a deliberate false positive:
|
|
59
|
+
* `Math.min(horizonDay, earliest + est.days)` makes the two cases
|
|
60
|
+
* identical from outside the scheduler, and over-caution is the safe
|
|
61
|
+
* direction when the alternative is inventing confidence.
|
|
62
|
+
*/
|
|
63
|
+
beyondHorizon: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The ship evidence on a bead, flattened for rendering. One field rather
|
|
68
|
+
* than a loose `mrUrl`, so a component cannot show the MR while being
|
|
69
|
+
* unaware that a commit is also recorded.
|
|
70
|
+
*/
|
|
71
|
+
export interface ShipRecord {
|
|
72
|
+
mrUrl: string | null;
|
|
73
|
+
commit: string | null;
|
|
74
|
+
/** First 8 characters — what a UI should print. */
|
|
75
|
+
shortCommit: string | null;
|
|
76
|
+
/**
|
|
77
|
+
* How the evidence was obtained. `reconstructed` means it was recovered
|
|
78
|
+
* after the fact — a SHA read out of a `Closes <id>` trailer is a fact,
|
|
79
|
+
* but the claim that it is *the* closing commit is an inference, and this
|
|
80
|
+
* gates velocity, the estimator and the trend.
|
|
81
|
+
*/
|
|
82
|
+
provenance: Provenance;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface BeadDetail {
|
|
86
|
+
id: string;
|
|
87
|
+
title: string;
|
|
88
|
+
status: string;
|
|
89
|
+
priority: number;
|
|
90
|
+
type: string;
|
|
91
|
+
assignee: string | null;
|
|
92
|
+
syntheticAssignee: boolean;
|
|
93
|
+
labels: string[];
|
|
94
|
+
dependencyCount: number;
|
|
95
|
+
blockingDeps: string[];
|
|
96
|
+
ship: ShipRecord | null;
|
|
97
|
+
/**
|
|
98
|
+
* Why a ship record that exists does not count as evidence — an empty
|
|
99
|
+
* object, or a `commit` that is not a SHA. Null when there is no record at
|
|
100
|
+
* all, or when the record is good. A UI that collapses those two nulls
|
|
101
|
+
* into one is hiding the case a person can actually fix.
|
|
102
|
+
*/
|
|
103
|
+
shipProblem: string | null;
|
|
104
|
+
estimateSource: string | null;
|
|
105
|
+
estimateDays: number | null;
|
|
106
|
+
scheduled: ScheduledWindow | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Assemble the tooltip payload. Pure — every field is a snapshot of the
|
|
111
|
+
* bead + schedule + estimate state at call time.
|
|
112
|
+
*/
|
|
113
|
+
export function buildBeadDetail(
|
|
114
|
+
bead: Bead,
|
|
115
|
+
schedule: ScheduleEntry | undefined,
|
|
116
|
+
estimate: EstimateResult | undefined,
|
|
117
|
+
opts: {
|
|
118
|
+
/** Day zero for the scheduler's offsets. Defaults to today. */
|
|
119
|
+
now?: Date;
|
|
120
|
+
/**
|
|
121
|
+
* Days from today at which the scheduler stopped projecting. A finish
|
|
122
|
+
* on or past it is a bound, not a date. Omit and nothing is treated as
|
|
123
|
+
* clamped, which is the right default for callers that have no horizon
|
|
124
|
+
* — better to lose the caveat than to invent one.
|
|
125
|
+
*/
|
|
126
|
+
horizonDay?: number;
|
|
127
|
+
} = {},
|
|
128
|
+
): BeadDetail {
|
|
129
|
+
const blockingDeps = (bead.dependencies ?? [])
|
|
130
|
+
.filter((d) => d.type === "blocks")
|
|
131
|
+
.map((d) => d.depends_on_id);
|
|
132
|
+
const evidence = shipEvidence(bead);
|
|
133
|
+
const ship: ShipRecord | null = evidence
|
|
134
|
+
? {
|
|
135
|
+
mrUrl: evidence.mrUrl,
|
|
136
|
+
commit: evidence.commit,
|
|
137
|
+
shortCommit: evidence.shortCommit,
|
|
138
|
+
provenance: evidence.provenance,
|
|
139
|
+
}
|
|
140
|
+
: null;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
id: bead.id,
|
|
144
|
+
title: bead.title,
|
|
145
|
+
status: bead.status,
|
|
146
|
+
priority: bead.priority ?? 4,
|
|
147
|
+
type: bead.issue_type,
|
|
148
|
+
assignee: schedule?.assignee ?? bead.assignee ?? null,
|
|
149
|
+
syntheticAssignee: !!schedule?.synthetic_assignee,
|
|
150
|
+
labels: bead.labels ?? [],
|
|
151
|
+
dependencyCount: blockingDeps.length,
|
|
152
|
+
blockingDeps,
|
|
153
|
+
ship,
|
|
154
|
+
shipProblem: shipRecordProblem(bead),
|
|
155
|
+
estimateSource: estimate?.source ?? null,
|
|
156
|
+
estimateDays: estimate?.days ?? null,
|
|
157
|
+
scheduled: schedule
|
|
158
|
+
? scheduledWindow(schedule, opts.now, opts.horizonDay)
|
|
159
|
+
: null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Midnight local, so a day offset lands on a calendar day not a time. */
|
|
164
|
+
function startOfDay(d: Date): Date {
|
|
165
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function addDays(from: Date, days: number): Date {
|
|
169
|
+
const out = new Date(from);
|
|
170
|
+
out.setDate(out.getDate() + Math.round(days));
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function scheduledWindow(
|
|
175
|
+
schedule: ScheduleEntry,
|
|
176
|
+
now: Date | undefined,
|
|
177
|
+
horizonDay: number | undefined,
|
|
178
|
+
): ScheduledWindow {
|
|
179
|
+
const day0 = startOfDay(now ?? new Date());
|
|
180
|
+
const startDay = schedule.start_day;
|
|
181
|
+
const endDay = schedule.end_day;
|
|
182
|
+
return {
|
|
183
|
+
startDay,
|
|
184
|
+
endDay,
|
|
185
|
+
startDate: addDays(day0, startDay),
|
|
186
|
+
endDate: addDays(day0, endDay),
|
|
187
|
+
// A bead that starts and finishes on the same day still took a day.
|
|
188
|
+
spanDays: Math.max(1, Math.round(endDay - startDay)),
|
|
189
|
+
inFlight: startDay < 0,
|
|
190
|
+
workDays:
|
|
191
|
+
schedule.work_start_day !== undefined && schedule.work_end_day !== undefined
|
|
192
|
+
? Math.max(0, schedule.work_end_day - schedule.work_start_day)
|
|
193
|
+
: null,
|
|
194
|
+
waitBeforeDays: Math.max(0, schedule.wait_before_days ?? 0),
|
|
195
|
+
waitAfterDays: Math.max(0, schedule.wait_after_days ?? 0),
|
|
196
|
+
beyondHorizon: horizonDay !== undefined && endDay >= horizonDay,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* An estimate as a person would say it: hours below a day, days above.
|
|
202
|
+
*
|
|
203
|
+
* Not `days.toFixed(1)`, which was throwing away most of the model. Once
|
|
204
|
+
* bp-67g.24 removed the estimator's half-day floor the estimates spread
|
|
205
|
+
* across 22 distinct values on a 577-bead repo, and rounding to a tenth of
|
|
206
|
+
* a day collapsed them back to 10 — "0.1d" for both 3.5 and 3.6 hours, and
|
|
207
|
+
* "0.2d" for everything from 4.5 to 5.7. Sub-day work wants sub-day units.
|
|
208
|
+
*
|
|
209
|
+
* Reuses economics.ts's formatter so an estimate and a recorded duration
|
|
210
|
+
* are spoken the same way in the same drawer.
|
|
211
|
+
*/
|
|
212
|
+
export function formatEstimate(days: number): string {
|
|
213
|
+
return formatDays(days);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── explanations (bp-67g.47) ───────────────────────────────────────────────
|
|
217
|
+
// Next to the shapes they describe, so a change to how the finish is clamped
|
|
218
|
+
// or the estimate sourced cannot leave a stale sentence behind in a component.
|
|
219
|
+
|
|
220
|
+
/** Why the drawer says "Projected finish" or "Finishes after" for this window. */
|
|
221
|
+
export function explainFinish(w: ScheduledWindow): string {
|
|
222
|
+
if (w.beyondHorizon) {
|
|
223
|
+
return "The scheduler's plan for this bead ran past the end of the scheduling window, so the date shown is the window's end — a bound (no sooner than this), not a projection. Widen the window preset and the date moves.";
|
|
224
|
+
}
|
|
225
|
+
const waits = w.waitAfterDays > 0 || w.waitBeforeDays > 0
|
|
226
|
+
? " The date includes this project's median waiting — unclaimed time before the work when nobody has claimed it yet, review time after — from its own history; the bar shows only the work."
|
|
227
|
+
: " No waiting is added: this project has no measured wait yet, so the landing is floored at its median lead time instead.";
|
|
228
|
+
return (w.inFlight
|
|
229
|
+
? "Where the scheduler's plan has this bead landing. It is already under way: the work is the estimate less the time since it started, packed into its assignee's daily work budget."
|
|
230
|
+
: "Where the scheduler's plan has this bead landing: everyone's ready work in priority order, each person's beads packed into their daily work budget — several short ones fit in a day — within their calibrated cap.") + waits;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The "Starts" row. */
|
|
234
|
+
export const EXPLAIN_STARTS =
|
|
235
|
+
"The day the scheduler's plan has this bead starting: once every blocker is done and its assignee has a free slot under their cap. A plan, not a commitment anyone made.";
|
|
236
|
+
|
|
237
|
+
/** What the estimate's source string means; null for a shape this build does not write. */
|
|
238
|
+
export function explainEstimate(detail: BeadDetail): string | null {
|
|
239
|
+
return detail.estimateSource ? describeEstimateSource(detail.estimateSource) : null;
|
|
240
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// changelog-view.ts — what the in-app changelog viewer shows (bp-67g.46).
|
|
2
|
+
//
|
|
3
|
+
// The file is CHANGELOG.md, inlined into the bundle at build time by a Vite
|
|
4
|
+
// raw import from the same source tree as the running code. That settles two
|
|
5
|
+
// of the bead's open questions at once:
|
|
6
|
+
//
|
|
7
|
+
// HOW IT REACHES THE BROWSER — no endpoint. The bundle carries it, so a
|
|
8
|
+
// published install has its own copy (CHANGELOG.md is in package.json
|
|
9
|
+
// "files") and reading it needs no network.
|
|
10
|
+
// NEWER THAN THE RUNNING BUILD — impossible by construction. Every entry
|
|
11
|
+
// in an inlined file shipped with the build that shows it, so nothing
|
|
12
|
+
// can be marked as newer. What the viewer says instead is the running
|
|
13
|
+
// version and what "Unreleased" means: shipped since the last tagged
|
|
14
|
+
// release, and in this build.
|
|
15
|
+
//
|
|
16
|
+
// Pure: the parsing is testable without a bundle.
|
|
17
|
+
|
|
18
|
+
/** The file from its first release heading on; the generated preamble is for git, not for the app. */
|
|
19
|
+
export function changelogBody(md: string): string {
|
|
20
|
+
const i = md.search(/^## /m);
|
|
21
|
+
return i < 0 ? md : md.slice(i);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChangelogSummary {
|
|
25
|
+
/** The newest tagged release in the file, or null when none has been cut. */
|
|
26
|
+
latestRelease: string | null;
|
|
27
|
+
hasUnreleased: boolean;
|
|
28
|
+
/** The sentence the viewer prints under its title. */
|
|
29
|
+
note: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function describeChangelog(md: string, runningVersion: string): ChangelogSummary {
|
|
33
|
+
const hasUnreleased = /^## \[Unreleased\]/m.test(md);
|
|
34
|
+
const m = /^## \[(\d[^\]]*)\]/m.exec(md);
|
|
35
|
+
const latestRelease = m ? m[1] : null;
|
|
36
|
+
const v = runningVersion && runningVersion !== "unknown" ? `v${runningVersion}` : "this build";
|
|
37
|
+
const parts = [`Everything here is in ${v}: the list ships with the build that shows it.`];
|
|
38
|
+
if (hasUnreleased) parts.push("Unreleased is what has shipped since the last tagged release.");
|
|
39
|
+
if (!latestRelease) parts.push("No release has been tagged yet.");
|
|
40
|
+
return { latestRelease, hasUnreleased, note: parts.join(" ") };
|
|
41
|
+
}
|